From a68c90c51229d9586cd5ff38cbf565006cc01984 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Fri, 22 Jun 2018 21:04:07 +0300 Subject: [PATCH 01/39] avoid calling exchange.get_fee inside loop --- freqtrade/optimize/backtesting.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 9a19d1412..ffb808a24 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -62,6 +62,7 @@ class Backtesting(object): self.config['exchange']['uid'] = '' self.config['dry_run'] = True self.exchange = Exchange(self.config) + self.fee = self.exchange.get_fee() @staticmethod def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]: @@ -130,14 +131,13 @@ class Backtesting(object): stake_amount = args['stake_amount'] max_open_trades = args.get('max_open_trades', 0) - fee = self.exchange.get_fee() trade = Trade( open_rate=buy_row.close, open_date=buy_row.date, stake_amount=stake_amount, amount=stake_amount / buy_row.open, - fee_open=fee, - fee_close=fee + fee_open=self.fee, + fee_close=self.fee ) # calculate win/lose forwards from buy point From c1691f21f3e3887d5842b091656084cc07de9088 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Fri, 22 Jun 2018 21:55:09 +0300 Subject: [PATCH 02/39] check that we set fee on backtesting init --- freqtrade/tests/optimize/test_backtesting.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 1702818b1..22536e42f 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -298,6 +298,7 @@ def test_backtesting_init(mocker, default_conf) -> None: Test Backtesting._init() method """ patch_exchange(mocker) + get_fee = mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5)) backtesting = Backtesting(default_conf) assert backtesting.config == default_conf assert isinstance(backtesting.analyze, Analyze) @@ -305,6 +306,8 @@ def test_backtesting_init(mocker, default_conf) -> None: assert callable(backtesting.tickerdata_to_dataframe) assert callable(backtesting.populate_buy_trend) assert callable(backtesting.populate_sell_trend) + get_fee.assert_called() + assert backtesting.fee == 0.5 def test_tickerdata_to_dataframe(default_conf, mocker) -> None: From 01d45bee76addbec4b66c999ae12d17409d9016e Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Sat, 23 Jun 2018 07:58:25 +0300 Subject: [PATCH 03/39] fix flake8 --- freqtrade/tests/optimize/test_backtesting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 22536e42f..65aa00a70 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -307,7 +307,7 @@ def test_backtesting_init(mocker, default_conf) -> None: assert callable(backtesting.populate_buy_trend) assert callable(backtesting.populate_sell_trend) get_fee.assert_called() - assert backtesting.fee == 0.5 + assert backtesting.fee == 0.5 def test_tickerdata_to_dataframe(default_conf, mocker) -> None: From 2738d3aed85ea5088af595751590a5000a190c68 Mon Sep 17 00:00:00 2001 From: xmatthias Date: Sat, 23 Jun 2018 13:12:36 +0200 Subject: [PATCH 04/39] update plotly --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a31976c8c..e26c9c8f6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,4 +22,4 @@ tabulate==0.8.2 coinmarketcap==5.0.3 # Required for plotting data -#plotly==2.3.0 +#plotly==2.7.0 From 5aae215c94cbe37b3981e51a6b460e36a7fbfa8e Mon Sep 17 00:00:00 2001 From: gcarq Date: Sat, 23 Jun 2018 10:25:03 +0200 Subject: [PATCH 05/39] wrap strategies with HyperoptStrategy for module lookups with pickle --- freqtrade/optimize/hyperopt.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 19e732d7b..e52581491 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -11,6 +11,7 @@ import pickle import signal import sys from argparse import Namespace +from copy import deepcopy from functools import reduce from math import exp from operator import itemgetter @@ -26,10 +27,25 @@ from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration from freqtrade.optimize import load_data from freqtrade.optimize.backtesting import Backtesting +from freqtrade.strategy.interface import IStrategy logger = logging.getLogger(__name__) +HyperoptStrategy = None + + +def wrap_strategy(strategy: IStrategy) -> Optional[HyperoptStrategy]: + """Wraps a given Strategy instance to HyperoptStrategy""" + global HyperoptStrategy + + attr = deepcopy(dict(strategy.__class__.__dict__)) + # Patch module name to make it compatible with pickle + attr['__module__'] = 'freqtrade.optimize.hyperopt' + HyperoptStrategy = type('HyperoptStrategy', (IStrategy,), attr) + return HyperoptStrategy() + + class Hyperopt(Backtesting): """ Hyperopt class, this class contains all the logic to run a hyperopt simulation @@ -39,7 +55,6 @@ class Hyperopt(Backtesting): hyperopt.start() """ def __init__(self, config: Dict[str, Any]) -> None: - super().__init__(config) # set TARGET_TRADES to suit your number concurrent trades so its realistic # to the number of days @@ -57,6 +72,9 @@ class Hyperopt(Backtesting): # check that the reported Σ% values do not exceed this! self.expected_max_profit = 3.0 + # Wrap strategy to make it compatible with pickle + self.analyze.strategy = wrap_strategy(self.analyze.strategy) + # Configuration and data used by hyperopt self.processed: Optional[Dict[str, Any]] = None From 78f50a14713c70c1b444e9c2ec0ec7569230f25e Mon Sep 17 00:00:00 2001 From: gcarq Date: Sat, 23 Jun 2018 11:13:49 +0200 Subject: [PATCH 06/39] move logic from hyperopt to freqtrade.strategy --- freqtrade/optimize/hyperopt.py | 19 ------------------- freqtrade/strategy/__init__.py | 32 ++++++++++++++++++++++++++++++++ freqtrade/strategy/resolver.py | 5 +++-- 3 files changed, 35 insertions(+), 21 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index e52581491..7a313a3ac 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -11,7 +11,6 @@ import pickle import signal import sys from argparse import Namespace -from copy import deepcopy from functools import reduce from math import exp from operator import itemgetter @@ -27,25 +26,10 @@ from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration from freqtrade.optimize import load_data from freqtrade.optimize.backtesting import Backtesting -from freqtrade.strategy.interface import IStrategy logger = logging.getLogger(__name__) -HyperoptStrategy = None - - -def wrap_strategy(strategy: IStrategy) -> Optional[HyperoptStrategy]: - """Wraps a given Strategy instance to HyperoptStrategy""" - global HyperoptStrategy - - attr = deepcopy(dict(strategy.__class__.__dict__)) - # Patch module name to make it compatible with pickle - attr['__module__'] = 'freqtrade.optimize.hyperopt' - HyperoptStrategy = type('HyperoptStrategy', (IStrategy,), attr) - return HyperoptStrategy() - - class Hyperopt(Backtesting): """ Hyperopt class, this class contains all the logic to run a hyperopt simulation @@ -72,9 +56,6 @@ class Hyperopt(Backtesting): # check that the reported Σ% values do not exceed this! self.expected_max_profit = 3.0 - # Wrap strategy to make it compatible with pickle - self.analyze.strategy = wrap_strategy(self.analyze.strategy) - # Configuration and data used by hyperopt self.processed: Optional[Dict[str, Any]] = None diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index e69de29bb..e1dc7bb3f 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -0,0 +1,32 @@ +import logging +from copy import deepcopy + +from freqtrade.strategy.interface import IStrategy + + +logger = logging.getLogger(__name__) + + +def import_strategy(strategy: IStrategy) -> IStrategy: + """ + Imports given Strategy instance to global scope + of freqtrade.strategy and returns an instance of it + """ + # Copy all attributes from base class and class + attr = deepcopy({**strategy.__class__.__dict__, **strategy.__dict__}) + # Adjust module name + attr['__module__'] = 'freqtrade.strategy' + + name = strategy.__class__.__name__ + clazz = type(name, (IStrategy,), attr) + + logger.debug( + 'Imported strategy %s.%s as %s.%s', + strategy.__module__, strategy.__class__.__name__, + clazz.__module__, strategy.__class__.__name__, + ) + + # Modify global scope to declare class + globals()[name] = clazz + + return clazz() diff --git a/freqtrade/strategy/resolver.py b/freqtrade/strategy/resolver.py index 3fd39bca3..3c7836291 100644 --- a/freqtrade/strategy/resolver.py +++ b/freqtrade/strategy/resolver.py @@ -11,6 +11,7 @@ from collections import OrderedDict from typing import Optional, Dict, Type from freqtrade import constants +from freqtrade.strategy import import_strategy from freqtrade.strategy.interface import IStrategy @@ -83,7 +84,7 @@ class StrategyResolver(object): strategy = self._search_strategy(path, strategy_name) if strategy: logger.info('Using resolved strategy %s from \'%s\'', strategy_name, path) - return strategy + return import_strategy(strategy) raise ImportError( "Impossible to load Strategy '{}'. This class does not exist" @@ -100,7 +101,7 @@ class StrategyResolver(object): """ # Generate spec based on absolute path - spec = importlib.util.spec_from_file_location('user_data.strategies', module_path) + spec = importlib.util.spec_from_file_location('unknown', module_path) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) # type: ignore # importlib does not use typehints From 398b21a11d175d02622bc3fb88ad38e52e04bbaf Mon Sep 17 00:00:00 2001 From: gcarq Date: Sat, 23 Jun 2018 11:14:31 +0200 Subject: [PATCH 07/39] implement test for import_strategy --- freqtrade/tests/strategy/test_strategy.py | 27 +++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index 244910790..26cd798f4 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -5,10 +5,34 @@ import os import pytest +from freqtrade.strategy import import_strategy +from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.resolver import StrategyResolver +def test_import_strategy(caplog): + caplog.set_level(logging.DEBUG) + + strategy = DefaultStrategy() + strategy.some_method = lambda *args, **kwargs: 42 + + assert strategy.__module__ == 'freqtrade.strategy.default_strategy' + assert strategy.some_method() == 42 + + imported_strategy = import_strategy(strategy) + + assert imported_strategy.__module__ == 'freqtrade.strategy' + assert imported_strategy.some_method() == 42 + + assert ( + 'freqtrade.strategy', + logging.DEBUG, + 'Imported strategy freqtrade.strategy.default_strategy.DefaultStrategy ' + 'as freqtrade.strategy.DefaultStrategy', + ) in caplog.record_tuples + + def test_search_strategy(): default_location = os.path.join(os.path.dirname( os.path.realpath(__file__)), '..', '..', 'strategy' @@ -20,8 +44,7 @@ def test_search_strategy(): def test_load_strategy(result): - resolver = StrategyResolver() - resolver._load_strategy('TestStrategy') + resolver = StrategyResolver({'strategy': 'TestStrategy'}) assert hasattr(resolver.strategy, 'populate_indicators') assert 'adx' in resolver.strategy.populate_indicators(result) From 810d7de8693ddbc4e0d3acf14c7684e9f51cd9dc Mon Sep 17 00:00:00 2001 From: gcarq Date: Sat, 23 Jun 2018 11:57:26 +0200 Subject: [PATCH 08/39] tests: add dir() assertion --- freqtrade/tests/strategy/test_strategy.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index 26cd798f4..13eac3261 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -1,5 +1,4 @@ # pragma pylint: disable=missing-docstring, protected-access, C0103 - import logging import os @@ -22,6 +21,8 @@ def test_import_strategy(caplog): imported_strategy = import_strategy(strategy) + assert dir(strategy) == dir(imported_strategy) + assert imported_strategy.__module__ == 'freqtrade.strategy' assert imported_strategy.some_method() == 42 From b485e6e0ba151d23a41d5d0bcbf11be9e22ae827 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Mon, 18 Jun 2018 22:40:36 +0300 Subject: [PATCH 09/39] start small --- freqtrade/optimize/hyperopt.py | 185 ++------------------------------- 1 file changed, 6 insertions(+), 179 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 7a313a3ac..31764fbd7 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -21,6 +21,8 @@ import talib.abstract as ta from hyperopt import STATUS_FAIL, STATUS_OK, Trials, fmin, hp, space_eval, tpe from pandas import DataFrame +from skopt.space import Real, Integer, Categorical + import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration @@ -65,121 +67,18 @@ class Hyperopt(Backtesting): @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 @@ -295,38 +194,6 @@ class Hyperopt(Backtesting): {'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'}, - ]), } def has_space(self, space: str) -> bool: @@ -361,12 +228,8 @@ 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']: @@ -375,49 +238,13 @@ class Hyperopt(Backtesting): 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) # 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( - dataframe['macd'], dataframe['macdsignal'] - )), - 'sar_reversal': (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'])) + #conditions.append(triggers.get(params['trigger']['type'])) + + conditions.append(dataframe['close'] < dataframe['bb_lowerband']) # single trigger dataframe.loc[ reduce(lambda x, y: x & y, conditions), From 0cb1aedf5bd7a6f746d68319d8f6bff327efbf2d Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Tue, 19 Jun 2018 09:09:54 +0300 Subject: [PATCH 10/39] problem with pickling --- freqtrade/optimize/hyperopt.py | 157 +++++++++++++++++++-------------- 1 file changed, 91 insertions(+), 66 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 31764fbd7..07e618b15 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -10,6 +10,8 @@ import os import pickle import signal import sys +import multiprocessing + from argparse import Namespace from functools import reduce from math import exp @@ -22,6 +24,8 @@ from hyperopt import STATUS_FAIL, STATUS_OK, Trials, fmin, hp, space_eval, tpe from pandas import DataFrame from skopt.space import Real, Integer, Categorical +from skopt import Optimizer +from sklearn.externals.joblib import Parallel, delayed import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.arguments import Arguments @@ -65,6 +69,21 @@ class Hyperopt(Backtesting): 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): + msg = "Mismatch in number of search-space dimensions. " \ + "len(dimensions)=={} and len(x)=={}" + msg = msg.format(len(dimensions), len(params)) + raise ValueError(msg) + + # 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: dataframe['adx'] = ta.ADX(dataframe) @@ -173,28 +192,17 @@ class Hyperopt(Backtesting): """ 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)} - ]), - } + 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'), + ] + def has_space(self, space: str) -> bool: """ @@ -208,14 +216,15 @@ class Hyperopt(Backtesting): """ Return the space to use during Hyperopt """ - spaces: Dict = {} - if self.has_space('buy'): - spaces = {**spaces, **Hyperopt.indicator_space()} - if self.has_space('roi'): - spaces = {**spaces, **Hyperopt.roi_space()} - if self.has_space('stoploss'): - spaces = {**spaces, **Hyperopt.stoploss_space()} - return spaces + return Hyperopt.indicator_space() + # spaces: Dict = {} + # if self.has_space('buy'): + # spaces = {**spaces, **Hyperopt.indicator_space()} + # if self.has_space('roi'): + # spaces = {**spaces, **Hyperopt.roi_space()} + # if self.has_space('stoploss'): + # spaces = {**spaces, **Hyperopt.stoploss_space()} + # return spaces @staticmethod def buy_strategy_generator(params: Dict[str, Any]) -> Callable: @@ -228,16 +237,16 @@ class Hyperopt(Backtesting): """ conditions = [] # GUARDS AND TRENDS - if 'macd_below_zero' in params and params['macd_below_zero']['enabled']: - conditions.append(dataframe['macd'] < 0) - 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 'macd_below_zero' in params and params['macd_below_zero']['enabled']: +# conditions.append(dataframe['macd'] < 0) + if 'mfi-enabled' 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']) # TRIGGERS triggers = { @@ -254,7 +263,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) @@ -297,12 +308,13 @@ class Hyperopt(Backtesting): 'result': result_explanation, } ) + return loss - return { - 'loss': loss, - 'status': STATUS_OK, - 'result': result_explanation, - } +# return { +# 'loss': loss, +# 'status': STATUS_OK, +# 'result': result_explanation, +# } def format_results(self, results: DataFrame) -> str: """ @@ -347,16 +359,29 @@ class Hyperopt(Backtesting): ) try: - best_parameters = fmin( - fn=self.generate_optimizer, - space=self.hyperopt_space(), - algo=tpe.suggest, - max_evals=self.total_tries, - trials=self.trials - ) + # best_parameters = fmin( + # fn=self.generate_optimizer, + # space=self.hyperopt_space(), + # algo=tpe.suggest, + # max_evals=self.total_tries, + # trials=self.trials + # ) + + # results = sorted(self.trials.results, key=itemgetter('loss')) + # best_result = results[0]['result'] + cpus = multiprocessing.cpu_count() + print(f'Found {cpus}. Let\'s make them scream!') + + opt = Optimizer(self.hyperopt_space(), "ET", acq_optimizer="sampling") + + for i in range(self.total_tries//cpus): + asked = opt.ask(n_points=cpus) + #asked = opt.ask() + #f_val = self.generate_optimizer(asked) + f_val = Parallel(n_jobs=-1)(delayed(self.generate_optimizer)(v) for v in asked) + opt.tell(asked, f_val) + print(f'got value {f_val}') - results = sorted(self.trials.results, key=itemgetter('loss')) - best_result = results[0]['result'] except ValueError: best_parameters = {} @@ -364,20 +389,20 @@ class Hyperopt(Backtesting): 'try with more epochs (param: -e).' # Improve best parameter logging display - if best_parameters: - best_parameters = space_eval( - self.hyperopt_space(), - best_parameters - ) + # 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 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) + # logger.info('Best Result:\n%s', best_result) - # Store trials result to file to resume next time - self.save_trials() + # # Store trials result to file to resume next time + # self.save_trials() def signal_handler(self, sig, frame) -> None: """ From a46badd5c066f13bbbad1c38bf309c6346e965bf Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Tue, 19 Jun 2018 21:57:42 +0300 Subject: [PATCH 11/39] reuse pool workers --- freqtrade/optimize/hyperopt.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 07e618b15..e49e66137 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -308,13 +308,12 @@ class Hyperopt(Backtesting): 'result': result_explanation, } ) - return loss -# return { -# 'loss': loss, -# 'status': STATUS_OK, -# 'result': result_explanation, -# } + return { + 'loss': loss, + 'status': STATUS_OK, + 'result': result_explanation, + } def format_results(self, results: DataFrame) -> str: """ @@ -374,13 +373,14 @@ class Hyperopt(Backtesting): opt = Optimizer(self.hyperopt_space(), "ET", acq_optimizer="sampling") - for i in range(self.total_tries//cpus): - asked = opt.ask(n_points=cpus) - #asked = opt.ask() - #f_val = self.generate_optimizer(asked) - f_val = Parallel(n_jobs=-1)(delayed(self.generate_optimizer)(v) for v in asked) - opt.tell(asked, f_val) - print(f'got value {f_val}') + with Parallel(n_jobs=-1) as parallel: + for i in range(self.total_tries//cpus): + asked = opt.ask(n_points=cpus) + #asked = opt.ask() + #f_val = self.generate_optimizer(asked) + f_val = parallel(delayed(self.generate_optimizer)(v) for v in asked) + opt.tell(asked, [i['loss'] for i in f_val]) + print(f'got value {f_val}') except ValueError: From 964cbdc262115772b7fdb6f2322de0c93facfc0c Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Wed, 20 Jun 2018 09:13:51 +0300 Subject: [PATCH 12/39] increase initial sampling points --- freqtrade/optimize/hyperopt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index e49e66137..919526e4a 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -371,7 +371,7 @@ class Hyperopt(Backtesting): cpus = multiprocessing.cpu_count() print(f'Found {cpus}. Let\'s make them scream!') - opt = Optimizer(self.hyperopt_space(), "ET", acq_optimizer="sampling") + opt = Optimizer(self.hyperopt_space(), base_estimator="ET", acq_optimizer="auto", n_initial_points=30) with Parallel(n_jobs=-1) as parallel: for i in range(self.total_tries//cpus): From c415014153ab2e15291264e20af5a7f38db7adc6 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Thu, 21 Jun 2018 14:05:17 +0300 Subject: [PATCH 13/39] use multiple jobs in acq --- freqtrade/optimize/hyperopt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 919526e4a..57429af7c 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -371,7 +371,7 @@ class Hyperopt(Backtesting): cpus = multiprocessing.cpu_count() print(f'Found {cpus}. Let\'s make them scream!') - opt = Optimizer(self.hyperopt_space(), base_estimator="ET", acq_optimizer="auto", n_initial_points=30) + opt = Optimizer(self.hyperopt_space(), base_estimator="ET", acq_optimizer="auto", n_initial_points=30, acq_optimizer_kwargs={'n_jobs': -1}) with Parallel(n_jobs=-1) as parallel: for i in range(self.total_tries//cpus): From 8fee2e2409b87c6ee2964452439b3c431fd58019 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Thu, 21 Jun 2018 14:59:36 +0300 Subject: [PATCH 14/39] move result logging out from optimizer --- freqtrade/optimize/hyperopt.py | 72 ++++++++++++---------------------- 1 file changed, 25 insertions(+), 47 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 57429af7c..15c1a5e48 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -50,7 +50,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 @@ -288,27 +287,8 @@ 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() - return { - 'status': STATUS_FAIL, - 'loss': float('inf') - } - 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, @@ -357,36 +337,34 @@ class Hyperopt(Backtesting): self.total_tries ) - try: - # best_parameters = fmin( - # fn=self.generate_optimizer, - # space=self.hyperopt_space(), - # algo=tpe.suggest, - # max_evals=self.total_tries, - # trials=self.trials - # ) + # results = sorted(self.trials.results, key=itemgetter('loss')) + # best_result = results[0]['result'] + cpus = multiprocessing.cpu_count() + print(f'Found {cpus}. Let\'s make them scream!') - # results = sorted(self.trials.results, key=itemgetter('loss')) - # best_result = results[0]['result'] - cpus = multiprocessing.cpu_count() - print(f'Found {cpus}. Let\'s make them scream!') + opt = Optimizer( + self.hyperopt_space(), + base_estimator="ET", + acq_optimizer="auto", + n_initial_points=30, + acq_optimizer_kwargs={'n_jobs': -1} + ) - opt = Optimizer(self.hyperopt_space(), base_estimator="ET", acq_optimizer="auto", n_initial_points=30, acq_optimizer_kwargs={'n_jobs': -1}) + with Parallel(n_jobs=-1) as parallel: + for i in range(self.total_tries//cpus): + asked = opt.ask(n_points=cpus) + f_val = parallel(delayed(self.generate_optimizer)(v) for v in asked) + opt.tell(asked, [i['loss'] for i in f_val]) - with Parallel(n_jobs=-1) as parallel: - for i in range(self.total_tries//cpus): - asked = opt.ask(n_points=cpus) - #asked = opt.ask() - #f_val = self.generate_optimizer(asked) - f_val = parallel(delayed(self.generate_optimizer)(v) for v in asked) - opt.tell(asked, [i['loss'] for i in f_val]) - print(f'got value {f_val}') - - - except ValueError: - best_parameters = {} - best_result = 'Sorry, Hyperopt was not able to find good parameters. Please ' \ - 'try with more epochs (param: -e).' + 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'], + } + ) # Improve best parameter logging display # if best_parameters: From 8272120c3a4dae84f8935b22435c4721bdac0dd5 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Fri, 22 Jun 2018 07:10:37 +0300 Subject: [PATCH 15/39] convert stoploss and ROI search spaces to skopt format --- freqtrade/optimize/hyperopt.py | 51 +++++++++++++++++----------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 15c1a5e48..35fed3b7e 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -16,14 +16,14 @@ 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 Dict, Any, Callable, Optional, 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 skopt.space import Real, Integer, Categorical +from skopt.space import Real, Integer, Categorical, Dimension from skopt import Optimizer from sklearn.externals.joblib import Parallel, delayed @@ -164,27 +164,27 @@ 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]: @@ -211,19 +211,18 @@ 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 """ - return Hyperopt.indicator_space() - # spaces: Dict = {} - # if self.has_space('buy'): - # spaces = {**spaces, **Hyperopt.indicator_space()} - # if self.has_space('roi'): - # spaces = {**spaces, **Hyperopt.roi_space()} - # if self.has_space('stoploss'): - # spaces = {**spaces, **Hyperopt.stoploss_space()} - # return spaces + spaces: List[Dimension] = [] + if self.has_space('buy'): + spaces += Hyperopt.indicator_space() + if self.has_space('roi'): + spaces += Hyperopt.roi_space() + if self.has_space('stoploss'): + spaces += Hyperopt.stoploss_space() + return spaces @staticmethod def buy_strategy_generator(params: Dict[str, Any]) -> Callable: From a525cba8e9c94771606696788c9d50b29ee2f51f Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Fri, 22 Jun 2018 13:02:26 +0300 Subject: [PATCH 16/39] switch signal handler to try catch. fix pickling and formatting output --- freqtrade/optimize/hyperopt.py | 102 ++++++++++++--------------------- 1 file changed, 38 insertions(+), 64 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 35fed3b7e..960812248 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -8,7 +8,6 @@ import json import logging import os import pickle -import signal import sys import multiprocessing @@ -18,9 +17,7 @@ from math import exp from operator import itemgetter from typing import Dict, Any, Callable, Optional, 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 skopt.space import Real, Integer, Categorical, Dimension @@ -64,9 +61,9 @@ class Hyperopt(Backtesting): # Configuration and data used by hyperopt self.processed: Optional[Dict[str, Any]] = None - # Hyperopt Trials + # Previous evaluations self.trials_file = os.path.join('user_data', 'hyperopt_trials.pickle') - self.trials = Trials() + self.trials = [] def get_args(self, params): dimensions = self.hyperopt_space() @@ -104,10 +101,11 @@ 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) + pickle.dump(self.trials, open(self.trials_file, 'wb')) - def read_trials(self) -> Trials: + def read_trials(self) -> List: """ Read hyperopt trials file """ @@ -120,9 +118,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: """ @@ -202,7 +206,6 @@ class Hyperopt(Backtesting): Categorical([True, False], name='rsi-enabled'), ] - def has_space(self, space: str) -> bool: """ Tell if a space value is contained in the configuration @@ -251,7 +254,7 @@ class Hyperopt(Backtesting): } #conditions.append(triggers.get(params['trigger']['type'])) - conditions.append(dataframe['close'] < dataframe['bb_lowerband']) # single trigger + conditions.append(dataframe['close'] < dataframe['bb_lowerband']) # single trigger dataframe.loc[ reduce(lambda x, y: x & y, conditions), @@ -290,7 +293,7 @@ class Hyperopt(Backtesting): return { 'loss': loss, - 'status': STATUS_OK, + 'params': params, 'result': result_explanation, } @@ -322,22 +325,15 @@ class Hyperopt(Backtesting): self.analyze.populate_indicators = Hyperopt.populate_indicators # type: ignore self.processed = self.tickerdata_to_dataframe(data) - logger.info('Preparing Trials..') - signal.signal(signal.SIGINT, self.signal_handler) + logger.info('Preparing..') # 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 + 'Loaded %d previous evaluations from disk.', + len(self.trials) ) - # results = sorted(self.trials.results, key=itemgetter('loss')) - # best_result = results[0]['result'] cpus = multiprocessing.cpu_count() print(f'Found {cpus}. Let\'s make them scream!') @@ -349,50 +345,28 @@ class Hyperopt(Backtesting): acq_optimizer_kwargs={'n_jobs': -1} ) - with Parallel(n_jobs=-1) as parallel: - for i in range(self.total_tries//cpus): - asked = opt.ask(n_points=cpus) - f_val = parallel(delayed(self.generate_optimizer)(v) for v in asked) - opt.tell(asked, [i['loss'] for i in f_val]) + try: + with Parallel(n_jobs=-1) as parallel: + for i in range(self.total_tries//cpus): + asked = opt.ask(n_points=cpus) + f_val = parallel(delayed(self.generate_optimizer)(v) for v in asked) + opt.tell(asked, [i['loss'] for i in 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'], - } - ) - - # 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: From dde7df7fd31513d8cbb6b70527910d85fb46721c Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Fri, 22 Jun 2018 17:08:22 +0300 Subject: [PATCH 17/39] add scikit-optimize to dependencies --- requirements.txt | 3 +++ setup.py | 1 + 2 files changed, 4 insertions(+) diff --git a/requirements.txt b/requirements.txt index e26c9c8f6..5727e70c9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,5 +21,8 @@ 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 diff --git a/setup.py b/setup.py index ee6b7ae38..cd0574fa2 100644 --- a/setup.py +++ b/setup.py @@ -36,6 +36,7 @@ setup(name='freqtrade', 'tabulate', 'cachetools', 'coinmarketcap', + 'scikit-optimize', ], include_package_data=True, zip_safe=False, From e8f2e6956d71026224d28ebf6ee37445764b998a Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Fri, 22 Jun 2018 21:02:59 +0300 Subject: [PATCH 18/39] to avoid pickle problems, get rid of reference to exchange after initialization --- freqtrade/optimize/hyperopt.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 960812248..e53a00e4f 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -325,6 +325,8 @@ class Hyperopt(Backtesting): self.analyze.populate_indicators = Hyperopt.populate_indicators # type: ignore self.processed = self.tickerdata_to_dataframe(data) + self.exchange = None + logger.info('Preparing..') # read trials file if we have one if os.path.exists(self.trials_file) and os.path.getsize(self.trials_file) > 0: From 09261b11afa20af557a2b638bb952282a09dd5ac Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Sat, 23 Jun 2018 15:22:14 +0300 Subject: [PATCH 19/39] remove hyperopt and networkx from dependencies --- requirements.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 5727e70c9..5754bf925 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,9 +15,6 @@ 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 From 136456afc0d657b2a9fe6e732f999ffe97ed5622 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Sat, 23 Jun 2018 15:44:51 +0300 Subject: [PATCH 20/39] add three triggers to hyperopting --- freqtrade/optimize/hyperopt.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index e53a00e4f..ea92cd561 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -94,6 +94,7 @@ class Hyperopt(Backtesting): # Bollinger bands bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) dataframe['bb_lowerband'] = bollinger['lower'] + dataframe['sar'] = ta.SAR(dataframe) return dataframe @@ -204,6 +205,7 @@ class Hyperopt(Backtesting): 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: @@ -238,8 +240,6 @@ class Hyperopt(Backtesting): """ conditions = [] # GUARDS AND TRENDS -# if 'macd_below_zero' in params and params['macd_below_zero']['enabled']: -# conditions.append(dataframe['macd'] < 0) if 'mfi-enabled' in params and params['mfi-enabled']: conditions.append(dataframe['mfi'] < params['mfi-value']) if 'fastd' in params and params['fastd-enabled']: @@ -250,11 +250,16 @@ class Hyperopt(Backtesting): conditions.append(dataframe['rsi'] < params['rsi-value']) # TRIGGERS - triggers = { - } - #conditions.append(triggers.get(params['trigger']['type'])) - - conditions.append(dataframe['close'] < dataframe['bb_lowerband']) # single trigger + 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'] + )) + if params['trigger'] == 'sar_reversal': + conditions.append(qtpylib.crossed_above( + dataframe['close'], dataframe['sar'] + )) dataframe.loc[ reduce(lambda x, y: x & y, conditions), From ab9e2fcea08692aa30abe5f20a48082c3452d34c Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Sat, 23 Jun 2018 15:47:19 +0300 Subject: [PATCH 21/39] fix guard names to match search space --- freqtrade/optimize/hyperopt.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index ea92cd561..f59b884b8 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -242,11 +242,11 @@ class Hyperopt(Backtesting): # GUARDS AND TRENDS if 'mfi-enabled' in params and params['mfi-enabled']: conditions.append(dataframe['mfi'] < params['mfi-value']) - if 'fastd' in params and params['fastd-enabled']: + if 'fastd-enabled' in params and params['fastd-enabled']: conditions.append(dataframe['fastd'] < params['fastd-value']) - if 'adx' in params and params['adx-enabled']: + if 'adx-enabled' in params and params['adx-enabled']: conditions.append(dataframe['adx'] > params['adx-value']) - if 'rsi' in params and params['rsi-enabled']: + if 'rsi-enabled' in params and params['rsi-enabled']: conditions.append(dataframe['rsi'] < params['rsi-value']) # TRIGGERS From 642ad02316aaa2a934545c9fb0841aa8ee7d5e89 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Sat, 23 Jun 2018 15:56:38 +0300 Subject: [PATCH 22/39] remove unused import --- freqtrade/optimize/hyperopt.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index f59b884b8..f91647f6e 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -4,7 +4,6 @@ This module contains the hyperopt logic """ -import json import logging import os import pickle From 118a43cbb898658e3cfed8bb415d82d644daf944 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Sun, 24 Jun 2018 15:27:53 +0300 Subject: [PATCH 23/39] fixing tests for hyperopt --- freqtrade/optimize/hyperopt.py | 38 +++-- freqtrade/tests/optimize/test_hyperopt.py | 197 +++++----------------- 2 files changed, 61 insertions(+), 174 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index f91647f6e..115cce1f0 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -315,6 +315,18 @@ class Hyperopt(Backtesting): results.trade_duration.mean(), ) + def get_optimizer(self) -> Optimizer: + return Optimizer( + self.hyperopt_space(), + base_estimator="ET", + acq_optimizer="auto", + n_initial_points=30, + acq_optimizer_kwargs={'n_jobs': -1} + ) + + def run_optimizer_parallel(self, parallel, asked) -> List: + return parallel(delayed(self.generate_optimizer)(v) for v in asked) + def start(self) -> None: timerange = Arguments.parse_timerange(None if self.config.get( 'timerange') is None else str(self.config.get('timerange'))) @@ -341,33 +353,25 @@ class Hyperopt(Backtesting): ) cpus = multiprocessing.cpu_count() - print(f'Found {cpus}. Let\'s make them scream!') + logger.info(f'Found {cpus}. Let\'s make them scream!') - opt = Optimizer( - self.hyperopt_space(), - base_estimator="ET", - acq_optimizer="auto", - n_initial_points=30, - acq_optimizer_kwargs={'n_jobs': -1} - ) + opt = self.get_optimizer() try: with Parallel(n_jobs=-1) as parallel: for i in range(self.total_tries//cpus): asked = opt.ask(n_points=cpus) - f_val = parallel(delayed(self.generate_optimizer)(v) for v in asked) + f_val = self.run_optimizer_parallel(parallel, asked) opt.tell(asked, [i['loss'] for i in f_val]) 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'], - } - ) + 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..') diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 8ad1932af..0f3e56916 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -1,6 +1,5 @@ # pragma pylint: disable=missing-docstring,W0212,C0103 import os -import signal from copy import deepcopy from unittest.mock import MagicMock @@ -42,16 +41,7 @@ def create_trials(mocker) -> None: mocker.patch('freqtrade.optimize.hyperopt.os.remove', return_value=True) mocker.patch('freqtrade.optimize.hyperopt.pickle.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 @@ -148,74 +138,7 @@ 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 - - +@pytest.mark.skip(reason="Test not implemented") 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()) @@ -244,6 +167,7 @@ def test_fmin_throw_value_error(mocker, init_hyperopt, default_conf, caplog) -> assert line in caplog.text +@pytest.mark.skip(reason="Waits for fixing") def test_resuming_previous_hyperopt_results_succeeds(mocker, init_hyperopt, default_conf) -> None: trials = create_trials(mocker) @@ -286,17 +210,18 @@ def test_resuming_previous_hyperopt_results_succeeds(mocker, init_hyperopt, defa def test_save_trials_saves_trials(mocker, init_hyperopt, caplog) -> None: - create_trials(mocker) + trials = create_trials(mocker) mock_dump = mocker.patch('freqtrade.optimize.hyperopt.pickle.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() @@ -333,12 +258,14 @@ 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: 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 +274,12 @@ 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 def test_format_results(init_hyperopt): @@ -384,20 +312,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 +336,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 +351,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) @@ -505,33 +390,31 @@ def test_generate_optimizer(mocker, init_hyperopt, default_conf) -> None: patch_exchange(mocker) 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 From 17ee7f8be51ddb64b8c4ad1de88fc7edf266c647 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Mon, 25 Jun 2018 11:15:11 +0300 Subject: [PATCH 24/39] fix typo in requirements.txt --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5754bf925..c22dcbdca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,7 +19,7 @@ tabulate==0.8.2 coinmarketcap==5.0.3 # Required for hyperopt -scikit-optimize=0.5.2 +scikit-optimize==0.5.2 # Required for plotting data #plotly==2.7.0 From 0bddc58ec41db007c75de5b16d1589e66cc3131d Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Mon, 25 Jun 2018 11:38:14 +0300 Subject: [PATCH 25/39] extract loading previous results to a method --- freqtrade/optimize/hyperopt.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 115cce1f0..b8b53ab3c 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -327,6 +327,15 @@ class Hyperopt(Backtesting): 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'))) @@ -340,23 +349,13 @@ class Hyperopt(Backtesting): if self.has_space('buy'): self.analyze.populate_indicators = Hyperopt.populate_indicators # type: ignore self.processed = self.tickerdata_to_dataframe(data) - self.exchange = None - - logger.info('Preparing..') - # 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) - ) + self.load_previous_results() cpus = multiprocessing.cpu_count() logger.info(f'Found {cpus}. Let\'s make them scream!') opt = self.get_optimizer() - try: with Parallel(n_jobs=-1) as parallel: for i in range(self.total_tries//cpus): From 2b6407e5982dbeb8bd1038945507c7875592bfd4 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Mon, 25 Jun 2018 11:38:42 +0300 Subject: [PATCH 26/39] remove unused tests from hyperopt --- freqtrade/tests/optimize/test_hyperopt.py | 72 ----------------------- 1 file changed, 72 deletions(-) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 0f3e56916..9194a66c9 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -44,7 +44,6 @@ def create_trials(mocker) -> None: return [{'loss': 1, 'result': 'foo', 'params': {}}] -# Unit tests def test_start(mocker, default_conf, caplog) -> None: """ Test start() function @@ -138,77 +137,6 @@ def test_no_log_if_loss_does_not_improve(init_hyperopt, caplog) -> None: assert caplog.record_tuples == [] -@pytest.mark.skip(reason="Test not implemented") -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 - - -@pytest.mark.skip(reason="Waits for fixing") -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: trials = create_trials(mocker) mock_dump = mocker.patch('freqtrade.optimize.hyperopt.pickle.dump', return_value=None) From 0ce08932ed31edde076b91589526ffcd06f428be Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Sat, 30 Jun 2018 09:54:31 +0300 Subject: [PATCH 27/39] mypy fixes --- freqtrade/optimize/hyperopt.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index b8b53ab3c..eb9015356 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -62,7 +62,7 @@ class Hyperopt(Backtesting): # Previous evaluations self.trials_file = os.path.join('user_data', 'hyperopt_trials.pickle') - self.trials = [] + self.trials: List = [] def get_args(self, params): dimensions = self.hyperopt_space() @@ -191,7 +191,7 @@ class Hyperopt(Backtesting): ] @staticmethod - def indicator_space() -> Dict[str, Any]: + def indicator_space() -> List[Dimension]: """ Define your Hyperopt space for searching strategy parameters """ @@ -349,7 +349,7 @@ class Hyperopt(Backtesting): if self.has_space('buy'): self.analyze.populate_indicators = Hyperopt.populate_indicators # type: ignore self.processed = self.tickerdata_to_dataframe(data) - self.exchange = None + self.exchange = None # type: ignore self.load_previous_results() cpus = multiprocessing.cpu_count() From a58d51ded0fcfd06cbe7344da202e353e9deff10 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Mon, 2 Jul 2018 09:56:58 +0300 Subject: [PATCH 28/39] update hyperopt documentation --- docs/hyperopt.md | 279 ++++++++++++++++------------------------------- 1 file changed, 96 insertions(+), 183 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 2ad94896a..8d3fe6704 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -1,153 +1,109 @@ # Hyperopt This page explains how to tune your strategy by finding the optimal -parameters with Hyperopt. +parameters, process called hyperparameter optimization. The bot uses several +algorithms included in `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 +### 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`. +- 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`. +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. -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 + # 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. + +### Adding 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`. @@ -164,12 +120,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 +134,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 +149,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 creating a new strategy. +Given 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). From fa8fc3e4ce19b566a9ea5f079cb26db38b143089 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Mon, 2 Jul 2018 11:44:33 +0300 Subject: [PATCH 29/39] handle the case where we have zero buys --- freqtrade/optimize/hyperopt.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index eb9015356..e499def70 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -31,6 +31,8 @@ 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 + class Hyperopt(Backtesting): """ @@ -152,7 +154,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]: @@ -293,6 +296,13 @@ class Hyperopt(Backtesting): trade_count = len(results.index) trade_duration = results.trade_duration.mean() + if trade_count == 0: + return { + 'loss': MAX_LOSS, + 'params': params, + 'result': result_explanation, + } + loss = self.calculate_loss(total_profit, trade_count, trade_duration) return { From 79aab4cce2f516df4d9504d99c31f3ccf4b70394 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Tue, 3 Jul 2018 11:17:41 +0300 Subject: [PATCH 30/39] use fstring --- freqtrade/optimize/hyperopt.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index e499def70..9b26b5b09 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -71,10 +71,8 @@ class Hyperopt(Backtesting): # Ensure the number of dimensions match # the number of parameters in the list x. if len(params) != len(dimensions): - msg = "Mismatch in number of search-space dimensions. " \ - "len(dimensions)=={} and len(x)=={}" - msg = msg.format(len(dimensions), len(params)) - raise ValueError(msg) + 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. From 2713fdb8607b01602d34c37e7c31facc7f9cb78e Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Tue, 3 Jul 2018 11:46:56 +0300 Subject: [PATCH 31/39] use cpu count explicitly in job count --- freqtrade/optimize/hyperopt.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 9b26b5b09..8290eb2d8 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -323,13 +323,13 @@ class Hyperopt(Backtesting): results.trade_duration.mean(), ) - def get_optimizer(self) -> Optimizer: + 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': -1} + acq_optimizer_kwargs={'n_jobs': cpu_count} ) def run_optimizer_parallel(self, parallel, asked) -> List: @@ -361,11 +361,11 @@ class Hyperopt(Backtesting): self.load_previous_results() cpus = multiprocessing.cpu_count() - logger.info(f'Found {cpus}. Let\'s make them scream!') + logger.info(f'Found {cpus} CPU cores. Let\'s make them scream!') - opt = self.get_optimizer() + opt = self.get_optimizer(cpus) try: - with Parallel(n_jobs=-1) as parallel: + with Parallel(n_jobs=cpus) as parallel: for i in range(self.total_tries//cpus): asked = opt.ask(n_points=cpus) f_val = self.run_optimizer_parallel(parallel, asked) From 4a26b88a17af950f2bdedcfde43e2cd684dfcd4d Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Tue, 3 Jul 2018 12:43:25 +0300 Subject: [PATCH 32/39] improve documentation --- docs/hyperopt.md | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 8d3fe6704..f4b69b632 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -1,7 +1,7 @@ # Hyperopt -This page explains how to tune your strategy by finding the optimal -parameters, process called hyperparameter optimization. The bot uses several -algorithms included in `scikit-optimize` package to accomplish this. The +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. @@ -17,18 +17,17 @@ and still take a long time. 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) ### 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: +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. +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". Hyperoptimization will, for each eval round, pick one trigger and possibly multiple guards. The constructed strategy will be something like @@ -103,9 +102,13 @@ with different value combinations. It will then use the given historical data an 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. -### Adding 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`. +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. @@ -150,8 +153,8 @@ Legal values are: - 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 creating a new strategy. -Given following result from hyperopt: +Once Hyperopt is completed you can use the result to create a new strategy. +Given the following result from hyperopt: ``` Best result: From ee4754cfb985fbe608ff02b21c65102223e1e005 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Tue, 3 Jul 2018 14:46:16 +0300 Subject: [PATCH 33/39] avoid re-serialization of whole dataframe --- freqtrade/optimize/hyperopt.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 8290eb2d8..57bf75742 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -14,14 +14,14 @@ from argparse import Namespace from functools import reduce from math import exp from operator import itemgetter -from typing import Dict, Any, Callable, Optional, List +from typing import Dict, Any, Callable, List import talib.abstract as ta from pandas import DataFrame from skopt.space import Real, Integer, Categorical, Dimension from skopt import Optimizer -from sklearn.externals.joblib import Parallel, delayed +from sklearn.externals.joblib import Parallel, delayed, load, dump import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.arguments import Arguments @@ -32,6 +32,7 @@ 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): @@ -60,7 +61,7 @@ class Hyperopt(Backtesting): self.expected_max_profit = 3.0 # Configuration and data used by hyperopt - self.processed: Optional[Dict[str, Any]] = None +# self.processed: Optional[Dict[str, Any]] = None # Previous evaluations self.trials_file = os.path.join('user_data', 'hyperopt_trials.pickle') @@ -281,10 +282,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), } ) @@ -356,7 +358,7 @@ 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() From ef59f9ad249f8a24a0ab29b2cb4c2f28d180c206 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Tue, 3 Jul 2018 21:50:24 +0300 Subject: [PATCH 34/39] sort imports in hyperopt.py --- freqtrade/optimize/hyperopt.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 57bf75742..a1d86bd18 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -5,23 +5,21 @@ This module contains the hyperopt logic """ import logging +import multiprocessing import os import pickle import sys -import multiprocessing - from argparse import Namespace from functools import reduce from math import exp from operator import itemgetter -from typing import Dict, Any, Callable, List +from typing import Any, Callable, Dict, List import talib.abstract as ta from pandas import DataFrame - -from skopt.space import Real, Integer, Categorical, Dimension +from sklearn.externals.joblib import Parallel, delayed, dump, load from skopt import Optimizer -from sklearn.externals.joblib import Parallel, delayed, load, dump +from skopt.space import Categorical, Dimension, Integer, Real import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.arguments import Arguments From 2cde540645bf321360594769768f017c2ceabe0f Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Tue, 3 Jul 2018 21:50:45 +0300 Subject: [PATCH 35/39] remove dead code --- freqtrade/optimize/hyperopt.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index a1d86bd18..861523965 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -58,9 +58,6 @@ 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_trials.pickle') self.trials: List = [] From 3a7056ea1bab0a2cea913e2eca3c6e590481ee93 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Tue, 3 Jul 2018 21:54:32 +0300 Subject: [PATCH 36/39] run at least one epoch --- freqtrade/optimize/hyperopt.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 861523965..71e923f00 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -361,9 +361,10 @@ class Hyperopt(Backtesting): 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: with Parallel(n_jobs=cpus) as parallel: - for i in range(self.total_tries//cpus): + 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]) From 9dbe0f50a3e6a708a621bea70757bf9e0b6f7577 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Tue, 3 Jul 2018 22:09:59 +0300 Subject: [PATCH 37/39] fix tests after changing the dumping and pickling dataframe in hyperopt --- freqtrade/tests/optimize/test_hyperopt.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 9194a66c9..bbe16ae7a 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -187,6 +187,7 @@ def test_roi_table_generation(init_hyperopt) -> None: 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( @@ -208,6 +209,7 @@ def test_start_calls_optimizer(mocker, init_hyperopt, default_conf, caplog) -> N parallel.assert_called_once() assert 'Best result:\nfoo result\nwith values:\n{}' in caplog.text + assert dumper.called def test_format_results(init_hyperopt): @@ -316,6 +318,7 @@ 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-value': 0, From c4a8435e007db87e157cf65e43213c2b4da81129 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Tue, 3 Jul 2018 22:17:43 +0300 Subject: [PATCH 38/39] change pickle file name to better suit it's current purpose --- freqtrade/optimize/hyperopt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 71e923f00..d90468cff 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -59,7 +59,7 @@ class Hyperopt(Backtesting): self.expected_max_profit = 3.0 # Previous evaluations - self.trials_file = os.path.join('user_data', 'hyperopt_trials.pickle') + self.trials_file = os.path.join('user_data', 'hyperopt_results.pickle') self.trials: List = [] def get_args(self, params): From 96bb2efe69ded2e6cbd2998804445e9d8726ded0 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Tue, 3 Jul 2018 22:51:48 +0300 Subject: [PATCH 39/39] use joblib.dump and load for trials --- freqtrade/optimize/hyperopt.py | 5 ++--- freqtrade/tests/optimize/test_hyperopt.py | 9 +++------ 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index d90468cff..7138e2601 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -7,7 +7,6 @@ This module contains the hyperopt logic import logging import multiprocessing import os -import pickle import sys from argparse import Namespace from functools import reduce @@ -99,14 +98,14 @@ class Hyperopt(Backtesting): """ if self.trials: logger.info('Saving %d evaluations to \'%s\'', len(self.trials), self.trials_file) - pickle.dump(self.trials, open(self.trials_file, 'wb')) + dump(self.trials, self.trials_file) 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 diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index bbe16ae7a..72a102c22 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -39,7 +39,7 @@ 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 [{'loss': 1, 'result': 'foo', 'params': {}}] @@ -139,10 +139,9 @@ def test_no_log_if_loss_does_not_improve(init_hyperopt, caplog) -> None: def test_save_trials_saves_trials(mocker, init_hyperopt, caplog) -> None: trials = create_trials(mocker) - mock_dump = mocker.patch('freqtrade.optimize.hyperopt.pickle.dump', return_value=None) + 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() @@ -157,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() @@ -168,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()