integrate hyperopt and implement subcommand

This commit is contained in:
gcarq 2017-11-25 01:04:11 +01:00
parent 7fa5846c6b
commit b9c4eafd96
9 changed files with 191 additions and 167 deletions

View File

@ -128,18 +128,20 @@ def parse_args(args: List[str]):
def build_subcommands(parser: argparse.ArgumentParser) -> None: def build_subcommands(parser: argparse.ArgumentParser) -> None:
""" Builds and attaches all subcommands """ """ Builds and attaches all subcommands """
from freqtrade.optimize import backtesting from freqtrade.optimize import backtesting, hyperopt
subparsers = parser.add_subparsers(dest='subparser') subparsers = parser.add_subparsers(dest='subparser')
backtest = subparsers.add_parser('backtesting', help='backtesting module')
backtest.set_defaults(func=backtesting.start) # Add backtesting subcommand
backtest.add_argument( backtesting_cmd = subparsers.add_parser('backtesting', help='backtesting module')
backtesting_cmd.set_defaults(func=backtesting.start)
backtesting_cmd.add_argument(
'-l', '--live', '-l', '--live',
action='store_true', action='store_true',
dest='live', dest='live',
help='using live data', help='using live data',
) )
backtest.add_argument( backtesting_cmd.add_argument(
'-i', '--ticker-interval', '-i', '--ticker-interval',
help='specify ticker interval in minutes (default: 5)', help='specify ticker interval in minutes (default: 5)',
dest='ticker_interval', dest='ticker_interval',
@ -147,13 +149,17 @@ def build_subcommands(parser: argparse.ArgumentParser) -> None:
type=int, type=int,
metavar='INT', metavar='INT',
) )
backtest.add_argument( backtesting_cmd.add_argument(
'--realistic-simulation', '--realistic-simulation',
help='uses max_open_trades from config to simulate real world limitations', help='uses max_open_trades from config to simulate real world limitations',
action='store_true', action='store_true',
dest='realistic_simulation', dest='realistic_simulation',
) )
# Add hyperopt subcommand
hyperopt_cmd = subparsers.add_parser('hyperopt', help='hyperopt module')
hyperopt_cmd.set_defaults(func=hyperopt.start)
# Required json-schema for user specified config # Required json-schema for user specified config
CONF_SCHEMA = { CONF_SCHEMA = {

View File

@ -1 +1,41 @@
from . import backtesting # pragma pylint: disable=missing-docstring
import json
import os
from typing import Optional, List, Dict
from pandas import DataFrame
from freqtrade.analyze import populate_indicators, parse_ticker_dataframe
def load_data(ticker_interval: int = 5, pairs: Optional[List[str]] = None) -> Dict[str, List]:
"""
Loads ticker history data for the given parameters
:param ticker_interval: ticker interval in minutes
:param pairs: list of pairs
:return: dict
"""
path = os.path.abspath(os.path.dirname(__file__))
result = {}
_pairs = pairs or [
'BTC_BCC', 'BTC_ETH', 'BTC_DASH', 'BTC_POWR', 'BTC_ETC',
'BTC_VTC', 'BTC_WAVES', 'BTC_LSK', 'BTC_XLM', 'BTC_OK',
]
for pair in _pairs:
with open('{abspath}/../tests/testdata/{pair}-{ticker_interval}.json'.format(
abspath=path,
pair=pair,
ticker_interval=ticker_interval,
)) as tickerdata:
result[pair] = json.load(tickerdata)
return result
def preprocess(tickerdata: Dict[str, List]) -> Dict[str, DataFrame]:
"""Creates a dataframe and populates indicators for given ticker data"""
processed = {}
for pair, pair_data in tickerdata.items():
processed[pair] = populate_indicators(parse_ticker_dataframe(pair_data))
return processed

View File

@ -9,34 +9,17 @@ from pandas import DataFrame
from tabulate import tabulate from tabulate import tabulate
from freqtrade import exchange from freqtrade import exchange
from freqtrade.analyze import parse_ticker_dataframe, populate_indicators, \ from freqtrade.analyze import populate_buy_trend, populate_sell_trend
populate_buy_trend, populate_sell_trend
from freqtrade.exchange import Bittrex from freqtrade.exchange import Bittrex
from freqtrade.main import min_roi_reached from freqtrade.main import min_roi_reached
from freqtrade.misc import load_config from freqtrade.misc import load_config
from freqtrade.optimize import load_data, preprocess
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.tests import load_backtesting_data
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def format_results(results: DataFrame):
return ('Made {:6d} buys. Average profit {: 5.2f}%. '
'Total profit was {: 7.3f}. Average duration {:5.1f} mins.').format(
len(results.index),
results.profit.mean() * 100.0,
results.profit.sum(),
results.duration.mean() * 5,
)
def preprocess(backdata) -> Dict[str, DataFrame]:
processed = {}
for pair, pair_data in backdata.items():
processed[pair] = populate_indicators(parse_ticker_dataframe(pair_data))
return processed
def get_timeframe(data: Dict[str, Dict]) -> Tuple[arrow.Arrow, arrow.Arrow]: def get_timeframe(data: Dict[str, Dict]) -> Tuple[arrow.Arrow, arrow.Arrow]:
""" """
Get the maximum timeframe for the given backtest data Get the maximum timeframe for the given backtest data
@ -151,7 +134,7 @@ def start(args):
data[pair] = exchange.get_ticker_history(pair, args.ticker_interval) data[pair] = exchange.get_ticker_history(pair, args.ticker_interval)
else: else:
print('Using local backtesting data (ignoring whitelist in given config)...') print('Using local backtesting data (ignoring whitelist in given config)...')
data = load_backtesting_data(args.ticker_interval) data = load_data(args.ticker_interval)
print('Using stake_currency: {} ...\nUsing stake_amount: {} ...'.format( print('Using stake_currency: {} ...\nUsing stake_amount: {} ...'.format(
config['stake_currency'], config['stake_amount'] config['stake_currency'], config['stake_amount']

View File

@ -1,29 +1,124 @@
# pragma pylint: disable=missing-docstring,W0212 # pragma pylint: disable=missing-docstring,W0212
import logging
import os
from functools import reduce from functools import reduce
from math import exp from math import exp
from operator import itemgetter from operator import itemgetter
import pytest
from hyperopt import fmin, tpe, hp, Trials, STATUS_OK from hyperopt import fmin, tpe, hp, Trials, STATUS_OK
from pandas import DataFrame from pandas import DataFrame
from freqtrade import exchange from freqtrade import exchange, optimize
from freqtrade.exchange import Bittrex from freqtrade.exchange import Bittrex
from freqtrade.optimize.backtesting import backtest, format_results from freqtrade.optimize.backtesting import backtest
from freqtrade.optimize.backtesting import preprocess
from freqtrade.tests import load_backtesting_data
from freqtrade.vendor.qtpylib.indicators import crossed_above from freqtrade.vendor.qtpylib.indicators import crossed_above
logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot
# set TARGET_TRADES to suit your number concurrent trades so its realistic to 20days of data # set TARGET_TRADES to suit your number concurrent trades so its realistic to 20days of data
TARGET_TRADES = 1100 TARGET_TRADES = 1100
TOTAL_TRIES = 4 TOTAL_TRIES = 4
# pylint: disable=C0103 # pylint: disable=C0103
current_tries = 0 current_tries = 0
# Configuration and data used by hyperopt
PROCESSED = optimize.preprocess(optimize.load_data())
OPTIMIZE_CONFIG = {
'max_open_trades': 3,
'stake_currency': 'BTC',
'stake_amount': 0.01,
'minimal_roi': {
'40': 0.0,
'30': 0.01,
'20': 0.02,
'0': 0.04,
},
'stoploss': -0.10,
}
SPACE = {
'mfi': hp.choice('mfi', [
{'enabled': False},
{'enabled': True, 'value': hp.quniform('mfi-value', 5, 25, 1)}
]),
'fastd': hp.choice('fastd', [
{'enabled': False},
{'enabled': True, 'value': hp.quniform('fastd-value', 10, 50, 1)}
]),
'adx': hp.choice('adx', [
{'enabled': False},
{'enabled': True, 'value': hp.quniform('adx-value', 15, 50, 1)}
]),
'rsi': hp.choice('rsi', [
{'enabled': False},
{'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 1)}
]),
'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': 'faststoch10'},
{'type': 'ao_cross_zero'},
{'type': 'ema5_cross_ema10'},
{'type': 'macd_cross_signal'},
{'type': 'sar_reversal'},
{'type': 'stochf_cross'},
{'type': 'ht_sine'},
]),
}
def optimizer(params):
from freqtrade.optimize import backtesting
backtesting.populate_buy_trend = buy_strategy_generator(params)
results = backtest(OPTIMIZE_CONFIG, PROCESSED)
result = format_results(results)
total_profit = results.profit.sum() * 1000
trade_count = len(results.index)
trade_loss = 1 - 0.35 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.2)
profit_loss = max(0, 1 - total_profit / 10000) # max profit 10000
# pylint: disable=W0603
global current_tries
current_tries += 1
print('{:5d}/{}: {}'.format(current_tries, TOTAL_TRIES, result))
return {
'loss': trade_loss + profit_loss,
'status': STATUS_OK,
'result': result
}
def format_results(results: DataFrame):
return ('Made {:6d} buys. Average profit {: 5.2f}%. '
'Total profit was {: 7.3f}. Average duration {:5.1f} mins.').format(
len(results.index),
results.profit.mean() * 100.0,
results.profit.sum(),
results.duration.mean() * 5,
)
def buy_strategy_generator(params): def buy_strategy_generator(params):
def populate_buy_trend(dataframe: DataFrame) -> DataFrame: def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
@ -70,94 +165,14 @@ def buy_strategy_generator(params):
return populate_buy_trend return populate_buy_trend
@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set") def start(args):
def test_hyperopt(backtest_conf, mocker): # TODO: parse args
mocked_buy_trend = mocker.patch('freqtrade.tests.test_backtesting.populate_buy_trend')
backdata = load_backtesting_data()
processed = preprocess(backdata)
exchange._API = Bittrex({'key': '', 'secret': ''}) exchange._API = Bittrex({'key': '', 'secret': ''})
def optimizer(params):
mocked_buy_trend.side_effect = buy_strategy_generator(params)
results = backtest(backtest_conf, processed, mocker)
result = format_results(results)
total_profit = results.profit.sum() * 1000
trade_count = len(results.index)
trade_loss = 1 - 0.35 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.2)
profit_loss = max(0, 1 - total_profit / 10000) # max profit 10000
# pylint: disable=W0603
global current_tries
current_tries += 1
print('{:5d}/{}: {}'.format(current_tries, TOTAL_TRIES, result))
return {
'loss': trade_loss + profit_loss,
'status': STATUS_OK,
'result': result
}
space = {
'mfi': hp.choice('mfi', [
{'enabled': False},
{'enabled': True, 'value': hp.quniform('mfi-value', 5, 25, 1)}
]),
'fastd': hp.choice('fastd', [
{'enabled': False},
{'enabled': True, 'value': hp.quniform('fastd-value', 10, 50, 1)}
]),
'adx': hp.choice('adx', [
{'enabled': False},
{'enabled': True, 'value': hp.quniform('adx-value', 15, 50, 1)}
]),
'rsi': hp.choice('rsi', [
{'enabled': False},
{'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 1)}
]),
'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': 'faststoch10'},
{'type': 'ao_cross_zero'},
{'type': 'ema5_cross_ema10'},
{'type': 'macd_cross_signal'},
{'type': 'sar_reversal'},
{'type': 'stochf_cross'},
{'type': 'ht_sine'},
]),
}
trials = Trials() trials = Trials()
best = fmin(fn=optimizer, space=space, algo=tpe.suggest, max_evals=TOTAL_TRIES, trials=trials) best = fmin(fn=optimizer, space=SPACE, algo=tpe.suggest, max_evals=TOTAL_TRIES, trials=trials)
print('\n\n\n\n==================== HYPEROPT BACKTESTING REPORT ==============================') print('\n\n\n\n==================== HYPEROPT BACKTESTING REPORT ==============================')
print('Best parameters {}'.format(best)) print('Best parameters {}'.format(best))
newlist = sorted(trials.results, key=itemgetter('loss')) newlist = sorted(trials.results, key=itemgetter('loss'))
print('Result: {}'.format(newlist[0]['result'])) print('Result: {}'.format(newlist[0]['result']))
if __name__ == '__main__':
# for profiling with cProfile and line_profiler
pytest.main([__file__, '-s'])

View File

@ -1,21 +0,0 @@
# pragma pylint: disable=missing-docstring
import json
import os
from typing import Optional, List
def load_backtesting_data(ticker_interval: int = 5, pairs: Optional[List[str]] = None):
path = os.path.abspath(os.path.dirname(__file__))
result = {}
_pairs = pairs or [
'BTC_BCC', 'BTC_ETH', 'BTC_DASH', 'BTC_POWR', 'BTC_ETC',
'BTC_VTC', 'BTC_WAVES', 'BTC_LSK', 'BTC_XLM', 'BTC_OK',
]
for pair in _pairs:
with open('{abspath}/testdata/{pair}-{ticker_interval}.json'.format(
abspath=path,
pair=pair,
ticker_interval=ticker_interval,
)) as tickerdata:
result[pair] = json.load(tickerdata)
return result

View File

@ -51,22 +51,6 @@ def default_conf():
return configuration return configuration
@pytest.fixture(scope="module")
def backtest_conf():
return {
"max_open_trades": 3,
"stake_currency": "BTC",
"stake_amount": 0.01,
"minimal_roi": {
"40": 0.0,
"30": 0.01,
"20": 0.02,
"0": 0.04
},
"stoploss": -0.10
}
@pytest.fixture @pytest.fixture
def update(): def update():
_update = Update(0) _update = Update(0)

View File

@ -78,10 +78,10 @@ def test_parse_args_backtesting(mocker):
def test_parse_args_backtesting_invalid(): def test_parse_args_backtesting_invalid():
with pytest.raises(SystemExit, match=r'2'): with pytest.raises(SystemExit, match=r'2'):
parse_args(['--ticker-interval']) parse_args(['backtesting --ticker-interval'])
with pytest.raises(SystemExit, match=r'2'): with pytest.raises(SystemExit, match=r'2'):
parse_args(['--ticker-interval', 'abc']) parse_args(['backtesting --ticker-interval', 'abc'])
def test_parse_args_backtesting_custom(mocker): def test_parse_args_backtesting_custom(mocker):
@ -99,6 +99,19 @@ def test_parse_args_backtesting_custom(mocker):
assert call_args.ticker_interval == 1 assert call_args.ticker_interval == 1
def test_parse_args_hyperopt(mocker):
hyperopt_mock = mocker.patch('freqtrade.optimize.hyperopt.start', MagicMock())
args = parse_args(['hyperopt'])
assert args is None
assert hyperopt_mock.call_count == 1
call_args = hyperopt_mock.call_args[0][0]
assert call_args.config == 'config.json'
assert call_args.loglevel == 20
assert call_args.subparser == 'hyperopt'
assert call_args.func is not None
def test_load_config(default_conf, mocker): def test_load_config(default_conf, mocker):
file_mock = mocker.patch('freqtrade.misc.open', mocker.mock_open( file_mock = mocker.patch('freqtrade.misc.open', mocker.mock_open(
read_data=json.dumps(default_conf) read_data=json.dumps(default_conf)

View File

@ -1,18 +1,16 @@
# pragma pylint: disable=missing-docstring,W0212 # pragma pylint: disable=missing-docstring,W0212
from freqtrade import exchange from freqtrade import exchange, optimize
from freqtrade.exchange import Bittrex from freqtrade.exchange import Bittrex
from freqtrade.optimize.backtesting import backtest, preprocess from freqtrade.optimize.backtesting import backtest
from freqtrade.tests import load_backtesting_data
def test_backtest(backtest_conf, mocker): def test_backtest(default_conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', backtest_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''}) exchange._API = Bittrex({'key': '', 'secret': ''})
data = load_backtesting_data(ticker_interval=5, pairs=['BTC_ETH']) data = optimize.load_data(ticker_interval=5, pairs=['BTC_ETH'])
results = backtest(backtest_conf, preprocess(data), 10, True) results = backtest(default_conf, optimize.preprocess(data), 10, True)
num_resutls = len(results) num_resutls = len(results)
assert num_resutls > 0 assert num_resutls > 0

View File

@ -0,0 +1,6 @@
# pragma pylint: disable=missing-docstring,W0212
def test_optimizer(default_conf, mocker):
# TODO: implement test
pass