Complete Backtesting and Hyperopt unit tests

This commit is contained in:
Gerald Lonlas 2018-03-02 21:46:32 +08:00
parent f4ec073099
commit 6ef7b7d93d
10 changed files with 851 additions and 648 deletions

View File

@ -30,7 +30,7 @@ class Analyze(object):
Init Analyze Init Analyze
:param config: Bot configuration (use the one from Configuration()) :param config: Bot configuration (use the one from Configuration())
""" """
self.logger = Logger(name=__name__).get_logger() self.logger = Logger(name=__name__, level=config.get('loglevel')).get_logger()
self.config = config self.config = config
self.strategy = Strategy(self.config) self.strategy = Strategy(self.config)

View File

@ -19,7 +19,8 @@ class Configuration(object):
""" """
def __init__(self, args: List[str]) -> None: def __init__(self, args: List[str]) -> None:
self.args = args self.args = args
self.logger = Logger(name=__name__).get_logger() self.logging = Logger(name=__name__)
self.logger = self.logging.get_logger()
self.config = self._load_config() self.config = self._load_config()
self.show_info() self.show_info()
@ -35,16 +36,24 @@ class Configuration(object):
config.update({'strategy': self.args.strategy}) config.update({'strategy': self.args.strategy})
# Add dynamic_whitelist if found # Add dynamic_whitelist if found
if self.args.dynamic_whitelist: if 'dynamic_whitelist' in self.args and self.args.dynamic_whitelist:
config.update({'dynamic_whitelist': self.args.dynamic_whitelist}) config.update({'dynamic_whitelist': self.args.dynamic_whitelist})
# Add dry_run_db if found and the bot in dry run # Add dry_run_db if found and the bot in dry run
if self.args.dry_run_db and config.get('dry_run', False): if self.args.dry_run_db and config.get('dry_run', False):
config.update({'dry_run_db': True}) config.update({'dry_run_db': True})
# Load Backtesting / Hyperopt # Log level
if 'loglevel' in self.args and self.args.loglevel:
config.update({'loglevel': self.args.loglevel})
self.logging.set_level(self.args.loglevel)
# Load Backtesting
config = self._load_backtesting_config(config) config = self._load_backtesting_config(config)
# Load Hyperopt
config = self._load_hyperopt_config(config)
return config return config
def _load_config_file(self, path: str) -> Dict[str, Any]: def _load_config_file(self, path: str) -> Dict[str, Any]:
@ -64,7 +73,7 @@ class Configuration(object):
def _load_backtesting_config(self, config: Dict[str, Any]) -> Dict[str, Any]: def _load_backtesting_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
""" """
Extract information for sys.argv and load Backtesting and Hyperopt configuration Extract information for sys.argv and load Backtesting configuration
:return: configuration as dictionary :return: configuration as dictionary
""" """
# If -i/--ticker-interval is used we override the configuration parameter # If -i/--ticker-interval is used we override the configuration parameter
@ -107,6 +116,24 @@ class Configuration(object):
return config return config
def _load_hyperopt_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
"""
Extract information for sys.argv and load Hyperopt configuration
:return: configuration as dictionary
"""
# If --realistic-simulation is used we add it to the configuration
if 'epochs' in self.args and self.args.epochs:
config.update({'epochs': self.args.epochs})
self.logger.info('Parameter --epochs detected ...')
self.logger.info('Will run Hyperopt with for %s epochs ...', config.get('epochs'))
# If --mongodb is used we add it to the configuration
if 'mongodb' in self.args and self.args.mongodb:
config.update({'mongodb': self.args.mongodb})
self.logger.info('Parameter --use-mongodb detected ...')
return config
def _validate_config(self, conf: Dict[str, Any]) -> Dict[str, Any]: def _validate_config(self, conf: Dict[str, Any]) -> Dict[str, Any]:
""" """
Validate the configuration follow the Config Schema Validate the configuration follow the Config Schema

View File

@ -19,9 +19,12 @@ class Logger(object):
:return: None :return: None
""" """
self.name = name self.name = name
self.level = level
self.logger = None self.logger = None
if level is None:
level = logging.INFO
self.level = level
self._init_logger() self._init_logger()
def _init_logger(self) -> None: def _init_logger(self) -> None:

View File

@ -5,7 +5,6 @@ This module contains the backtesting logic
""" """
from typing import Dict, Tuple, Any from typing import Dict, Tuple, Any
import logging
import arrow import arrow
from pandas import DataFrame, Series from pandas import DataFrame, Series
from tabulate import tabulate from tabulate import tabulate
@ -20,6 +19,7 @@ from freqtrade.logger import Logger
from freqtrade.misc import file_dump_json from freqtrade.misc import file_dump_json
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from memory_profiler import profile
class Backtesting(object): class Backtesting(object):
""" """
@ -30,7 +30,9 @@ class Backtesting(object):
backtesting.start() backtesting.start()
""" """
def __init__(self, config: Dict[str, Any]) -> None: def __init__(self, config: Dict[str, Any]) -> None:
self.logging = Logger(name=__name__)
# Init the logger
self.logging = Logger(name=__name__, level=config['loglevel'])
self.logger = self.logging.get_logger() self.logger = self.logging.get_logger()
self.config = config self.config = config
@ -219,6 +221,7 @@ class Backtesting(object):
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration'] labels = ['currency', 'profit_percent', 'profit_BTC', 'duration']
return DataFrame.from_records(trades, columns=labels) return DataFrame.from_records(trades, columns=labels)
@profile(precision=10)
def start(self) -> None: def start(self) -> None:
""" """
Run a backtesting end-to-end Run a backtesting end-to-end
@ -246,10 +249,14 @@ class Backtesting(object):
) )
max_open_trades = self.config.get('max_open_trades', 0) max_open_trades = self.config.get('max_open_trades', 0)
preprocessed = self.tickerdata_to_dataframe(data) preprocessed = self.tickerdata_to_dataframe(data)
# Print timeframe # Print timeframe
min_date, max_date = self.get_timeframe(preprocessed) min_date, max_date = self.get_timeframe(preprocessed)
import pprint
pprint.pprint(min_date)
pprint.pprint(max_date)
self.logger.info( self.logger.info(
'Measuring data from %s up to %s (%s days)..', 'Measuring data from %s up to %s (%s days)..',
min_date.isoformat(), min_date.isoformat(),

View File

@ -1,5 +1,8 @@
# pragma pylint: disable=missing-docstring,W0212,W0603 # pragma pylint: disable=too-many-instance-attributes, pointless-string-statement
"""
This module contains the hyperopt logic
"""
import json import json
import logging import logging
@ -19,49 +22,56 @@ from hyperopt.mongoexp import MongoTrials
from pandas import DataFrame from pandas import DataFrame
import freqtrade.vendor.qtpylib.indicators as qtpylib import freqtrade.vendor.qtpylib.indicators as qtpylib
# Monkey patch config from freqtrade.configuration import Configuration
from freqtrade import main # noqa; noqa from freqtrade.optimize import load_data
from freqtrade import exchange, misc, optimize from freqtrade.arguments import Arguments
from freqtrade.exchange import Bittrex from freqtrade.optimize.backtesting import Backtesting, setup_configuration
from freqtrade.misc import load_config from freqtrade.logger import Logger
from freqtrade.optimize import backtesting
from freqtrade.optimize.backtesting import backtest
from freqtrade.strategy.strategy import Strategy
from user_data.hyperopt_conf import hyperopt_optimize_conf from user_data.hyperopt_conf import hyperopt_optimize_conf
# Remove noisy log messages
logging.getLogger('hyperopt.mongoexp').setLevel(logging.WARNING)
logging.getLogger('hyperopt.tpe').setLevel(logging.WARNING)
logger = logging.getLogger(__name__) class Hyperopt(Backtesting):
"""
Hyperopt class, this class contains all the logic to run a hyperopt simulation
# set TARGET_TRADES to suit your number concurrent trades so its realistic to the number of days To run a backtest:
TARGET_TRADES = 600 hyperopt = Hyperopt(config)
TOTAL_TRIES = 0 hyperopt.start()
_CURRENT_TRIES = 0 """
CURRENT_BEST_LOSS = 100 def __init__(self, config: Dict[str, Any]) -> None:
# max average trade duration in minutes super().__init__(config)
# if eval ends with higher value, we consider it a failed eval
MAX_ACCEPTED_TRADE_DURATION = 300
# this is expexted avg profit * expected trade count # Rename the logging to display Hyperopt file instead of Backtesting
# for example 3.5%, 1100 trades, EXPECTED_MAX_PROFIT = 3.85 self.logging = Logger(name=__name__, level=config['loglevel'])
# check that the reported Σ% values do not exceed this! self.logger = self.logging.get_logger()
EXPECTED_MAX_PROFIT = 3.0
# Configuration and data used by hyperopt
PROCESSED = None # optimize.preprocess(optimize.load_data())
OPTIMIZE_CONFIG = hyperopt_optimize_conf()
# Hyperopt Trials
TRIALS_FILE = os.path.join('user_data', 'hyperopt_trials.pickle')
TRIALS = Trials()
main._CONF = OPTIMIZE_CONFIG
def populate_indicators(dataframe: DataFrame) -> DataFrame: # set TARGET_TRADES to suit your number concurrent trades so its realistic
# 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
# if eval ends with higher value, we consider it a failed eval
self.max_accepted_trade_duration = 300
# this is expexted avg profit * expected trade count
# for example 3.5%, 1100 trades, self.expected_max_profit = 3.85
# check that the reported Σ% values do not exceed this!
self.expected_max_profit = 3.0
# Configuration and data used by hyperopt
self.processed = None
# Hyperopt Trials
self.trials_file = os.path.join('user_data', 'hyperopt_trials.pickle')
self.trials = Trials()
@staticmethod
def populate_indicators(dataframe: DataFrame) -> DataFrame:
""" """
Adds several different TA indicators to the given DataFrame Adds several different TA indicators to the given DataFrame
""" """
@ -180,52 +190,61 @@ def populate_indicators(dataframe: DataFrame) -> DataFrame:
return dataframe return dataframe
def save_trials(self) -> None:
"""
Save hyperopt trials to file
"""
self.logger.info('Saving Trials to \'%s\'', self.trials_file)
pickle.dump(self.trials, open(self.trials_file, 'wb'))
def save_trials(trials, trials_path=TRIALS_FILE): def read_trials(self) -> Trials:
"""Save hyperopt trials to file""" """
logger.info('Saving Trials to \'{}\''.format(trials_path)) Read hyperopt trials file
pickle.dump(trials, open(trials_path, 'wb')) """
self.logger.info('Reading Trials from \'%s\'', self.trials_file)
trials = pickle.load(open(self.trials_file, 'rb'))
def read_trials(trials_path=TRIALS_FILE): os.remove(self.trials_file)
"""Read hyperopt trials file"""
logger.info('Reading Trials from \'{}\''.format(trials_path))
trials = pickle.load(open(trials_path, 'rb'))
os.remove(trials_path)
return trials return trials
def log_trials_result(self) -> None:
"""
Display Best hyperopt result
"""
vals = json.dumps(self.trials.best_trial['misc']['vals'], indent=4)
results = self.trials.best_trial['result']['result']
self.logger.info('Best result:\n%s\nwith values:\n%s', results, vals)
def log_trials_result(trials): def log_results(self, results) -> None:
vals = json.dumps(trials.best_trial['misc']['vals'], indent=4) """
results = trials.best_trial['result']['result'] Log results if it is better than any previous evaluation
logger.info('Best result:\n%s\nwith values:\n%s', results, vals) """
if results['loss'] < self.current_best_loss:
self.current_best_loss = results['loss']
def log_results(results): log_msg = '{:5d}/{}: {}. Loss {:.5f}'.format(
""" log results if it is better than any previous evaluation """
global CURRENT_BEST_LOSS
if results['loss'] < CURRENT_BEST_LOSS:
CURRENT_BEST_LOSS = results['loss']
logger.info('{:5d}/{}: {}. Loss {:.5f}'.format(
results['current_tries'], results['current_tries'],
results['total_tries'], results['total_tries'],
results['result'], results['result'],
results['loss'])) results['loss']
)
self.logger.info(log_msg)
else: else:
print('.', end='') print('.', end='')
sys.stdout.flush() sys.stdout.flush()
def calculate_loss(self, total_profit: float, trade_count: int, trade_duration: float) -> float:
def calculate_loss(total_profit: float, trade_count: int, trade_duration: float): """
""" objective function, returns smaller number for more optimal results """ Objective function, returns smaller number for more optimal results
trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8) """
profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT) trade_loss = 1 - 0.25 * exp(-(trade_count - self.target_trades) ** 2 / 10 ** 5.8)
duration_loss = 0.4 * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1) 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 return trade_loss + profit_loss + duration_loss
@staticmethod
def generate_roi_table(params) -> Dict[str, float]: def generate_roi_table(params) -> Dict[str, float]:
"""
Generate the ROI table thqt will be used by Hyperopt
"""
roi_table = {} roi_table = {}
roi_table["0"] = params['roi_p1'] + params['roi_p2'] + params['roi_p3'] roi_table["0"] = params['roi_p1'] + params['roi_p2'] + params['roi_p3']
roi_table[str(params['roi_t3'])] = params['roi_p1'] + params['roi_p2'] roi_table[str(params['roi_t3'])] = params['roi_p1'] + params['roi_p2']
@ -234,8 +253,11 @@ def generate_roi_table(params) -> Dict[str, float]:
return roi_table return roi_table
@staticmethod
def roi_space() -> Dict[str, Any]: def roi_space() -> Dict[str, Any]:
"""
Values to search for each ROI steps
"""
return { return {
'roi_t1': hp.quniform('roi_t1', 10, 120, 20), 'roi_t1': hp.quniform('roi_t1', 10, 120, 20),
'roi_t2': hp.quniform('roi_t2', 10, 60, 15), 'roi_t2': hp.quniform('roi_t2', 10, 60, 15),
@ -245,14 +267,17 @@ def roi_space() -> Dict[str, Any]:
'roi_p3': hp.quniform('roi_p3', 0.01, 0.20, 0.01), 'roi_p3': hp.quniform('roi_p3', 0.01, 0.20, 0.01),
} }
@staticmethod
def stoploss_space() -> Dict[str, Any]: def stoploss_space() -> Dict[str, Any]:
"""
Stoploss Value to search
"""
return { return {
'stoploss': hp.quniform('stoploss', -0.5, -0.02, 0.02), 'stoploss': hp.quniform('stoploss', -0.5, -0.02, 0.02),
} }
@staticmethod
def indicator_space() -> Dict[str, Any]: def indicator_space() -> Dict[str, Any]:
""" """
Define your Hyperopt space for searching strategy parameters Define your Hyperopt space for searching strategy parameters
""" """
@ -311,12 +336,19 @@ def indicator_space() -> Dict[str, Any]:
]), ]),
} }
@staticmethod
def hyperopt_space() -> Dict[str, Any]:
"""
Return the space to use during Hyperopt
"""
return {
**Hyperopt.indicator_space(),
**Hyperopt.roi_space(),
**Hyperopt.stoploss_space()
}
def hyperopt_space() -> Dict[str, Any]: @staticmethod
return {**indicator_space(), **roi_space(), **stoploss_space()} def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
""" """
Define the buy strategy parameters to be used by hyperopt Define the buy strategy parameters to be used by hyperopt
""" """
@ -389,42 +421,44 @@ def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
return populate_buy_trend return populate_buy_trend
def optimizer(self, params) -> Dict:
def optimizer(params):
global _CURRENT_TRIES
if 'roi_t1' in params: if 'roi_t1' in params:
strategy = Strategy() self.analyze.strategy.minimal_roi = self.generate_roi_table(params)
strategy.minimal_roi = generate_roi_table(params)
backtesting.populate_buy_trend = buy_strategy_generator(params) self.populate_buy_trend = self.buy_strategy_generator(params)
results = backtest({'stake_amount': OPTIMIZE_CONFIG['stake_amount'], results = self.backtest(
'processed': PROCESSED, {
'stoploss': params['stoploss']}) 'stake_amount': self.config['stake_amount'],
result_explanation = format_results(results) 'processed': self.processed,
'stoploss': params['stoploss']
}
)
result_explanation = self.format_results(results)
total_profit = results.profit_percent.sum() total_profit = results.profit_percent.sum()
trade_count = len(results.index) trade_count = len(results.index)
trade_duration = results.duration.mean() * 5 trade_duration = results.duration.mean() * 5
if trade_count == 0 or trade_duration > MAX_ACCEPTED_TRADE_DURATION: if trade_count == 0 or trade_duration > self.max_accepted_trade_duration:
print('.', end='') print('.', end='')
return { return {
'status': STATUS_FAIL, 'status': STATUS_FAIL,
'loss': float('inf') 'loss': float('inf')
} }
loss = calculate_loss(total_profit, trade_count, trade_duration) loss = self.calculate_loss(total_profit, trade_count, trade_duration)
_CURRENT_TRIES += 1 self.current_tries += 1
log_results({ self.log_results(
{
'loss': loss, 'loss': loss,
'current_tries': _CURRENT_TRIES, 'current_tries': self.current_tries,
'total_tries': TOTAL_TRIES, 'total_tries': self.total_tries,
'result': result_explanation, 'result': result_explanation,
}) }
)
return { return {
'loss': loss, 'loss': loss,
@ -432,8 +466,11 @@ def optimizer(params):
'result': result_explanation, 'result': result_explanation,
} }
@staticmethod
def format_results(results: DataFrame): def format_results(results: DataFrame) -> str:
"""
Return the format result in a string
"""
return ('{:6d} trades. Avg profit {: 5.2f}%. ' return ('{:6d} trades. Avg profit {: 5.2f}%. '
'Total profit {: 11.8f} BTC ({:.4f}Σ%). Avg duration {:5.1f} mins.').format( 'Total profit {: 11.8f} BTC ({:.4f}Σ%). Avg duration {:5.1f} mins.').format(
len(results.index), len(results.index),
@ -443,70 +480,56 @@ def format_results(results: DataFrame):
results.duration.mean() * 5, results.duration.mean() * 5,
) )
def start(self):
def start(args): timerange = Arguments.parse_timerange(self.config.get('timerange'))
global TOTAL_TRIES, PROCESSED, TRIALS, _CURRENT_TRIES data = load_data(
datadir=self.config.get('datadir'),
TOTAL_TRIES = args.epochs pairs=self.config['exchange']['pair_whitelist'],
ticker_interval=self.ticker_interval,
exchange._API = Bittrex({'key': '', 'secret': ''}) timerange=timerange
# Initialize logger
logging.basicConfig(
level=args.loglevel,
format='\n%(message)s',
) )
logger.info('Using config: %s ...', args.config) self.analyze.populate_indicators = Hyperopt.populate_indicators
config = load_config(args.config) self.processed = self.tickerdata_to_dataframe(data)
pairs = config['exchange']['pair_whitelist']
# If -i/--ticker-interval is use we override the configuration parameter if self.config.get('mongodb'):
# (that will override the strategy configuration) self.logger.info('Using mongodb ...')
if args.ticker_interval: self.logger.info(
config.update({'ticker_interval': args.ticker_interval}) 'Start scripts/start-mongodb.sh and start-hyperopt-worker.sh manually!'
)
# init the strategy to use
config.update({'strategy': args.strategy})
strategy = Strategy()
strategy.init(config)
timerange = misc.parse_timerange(args.timerange)
data = optimize.load_data(args.datadir, pairs=pairs,
ticker_interval=strategy.ticker_interval,
timerange=timerange)
optimize.populate_indicators = populate_indicators
PROCESSED = optimize.tickerdata_to_dataframe(data)
if args.mongodb:
logger.info('Using mongodb ...')
logger.info('Start scripts/start-mongodb.sh and start-hyperopt-worker.sh manually!')
db_name = 'freqtrade_hyperopt' db_name = 'freqtrade_hyperopt'
TRIALS = MongoTrials('mongo://127.0.0.1:1234/{}/jobs'.format(db_name), exp_key='exp1') self.trials = MongoTrials(
arg='mongo://127.0.0.1:1234/{}/jobs'.format(db_name),
exp_key='exp1'
)
else: else:
logger.info('Preparing Trials..') self.logger.info('Preparing Trials..')
signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGINT, self.signal_handler)
# read trials file if we have one # read trials file if we have one
if os.path.exists(TRIALS_FILE): if os.path.exists(self.trials_file):
TRIALS = read_trials() self.trials = self.read_trials()
_CURRENT_TRIES = len(TRIALS.results) self.current_tries = len(self.trials.results)
TOTAL_TRIES = TOTAL_TRIES + _CURRENT_TRIES self.total_tries += self.current_tries
logger.info( self.logger.info(
'Continuing with trials. Current: {}, Total: {}' 'Continuing with trials. Current: {}, Total: {}'
.format(_CURRENT_TRIES, TOTAL_TRIES)) .format(self.current_tries, self.total_tries)
try:
best_parameters = fmin(
fn=optimizer,
space=hyperopt_space(),
algo=tpe.suggest,
max_evals=TOTAL_TRIES,
trials=TRIALS
) )
results = sorted(TRIALS.results, key=itemgetter('loss')) try:
# change the Logging format
self.logging.set_format('\n%(message)s')
best_parameters = fmin(
fn=self.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'] best_result = results[0]['result']
except ValueError: except ValueError:
@ -517,23 +540,56 @@ def start(args):
# Improve best parameter logging display # Improve best parameter logging display
if best_parameters: if best_parameters:
best_parameters = space_eval( best_parameters = space_eval(
hyperopt_space(), self.hyperopt_space(),
best_parameters best_parameters
) )
logger.info('Best parameters:\n%s', json.dumps(best_parameters, indent=4)) self.logger.info('Best parameters:\n%s', json.dumps(best_parameters, indent=4))
if 'roi_t1' in best_parameters: if 'roi_t1' in best_parameters:
logger.info('ROI table:\n%s', generate_roi_table(best_parameters)) self.logger.info('ROI table:\n%s', self.generate_roi_table(best_parameters))
logger.info('Best Result:\n%s', best_result)
self.logger.info('Best Result:\n%s', best_result)
# Store trials result to file to resume next time # Store trials result to file to resume next time
save_trials(TRIALS) self.save_trials()
def signal_handler(self, sig, frame):
"""
Hyperopt SIGINT handler
"""
self.logger.info('Hyperopt received {}'.format(signal.Signals(sig).name))
def signal_handler(sig, frame): self.save_trials()
"""Hyperopt SIGINT handler""" self.log_trials_result()
logger.info('Hyperopt received {}'.format(signal.Signals(sig).name))
save_trials(TRIALS)
log_trials_result(TRIALS)
sys.exit(0) sys.exit(0)
def start(args) -> None:
"""
Start Backtesting script
:param args: Cli args from Arguments()
:return: None
"""
# Remove noisy log messages
logging.getLogger('hyperopt.mongoexp').setLevel(logging.WARNING)
logging.getLogger('hyperopt.tpe').setLevel(logging.WARNING)
# Initialize logger
logger = Logger(name=__name__).get_logger()
logger.info('Starting freqtrade in Hyperopt mode')
# Initialize configuration
#config = setup_configuration(args)
# Monkey patch of the configuration with hyperopt_conf.py
configuration = Configuration(args)
optimize_config = hyperopt_optimize_conf()
config = configuration._load_backtesting_config(optimize_config)
config = configuration._load_hyperopt_config(config)
config['exchange']['key'] = ''
config['exchange']['secret'] = ''
# Initialize backtesting object
hyperopt = Hyperopt(config)
hyperopt.start()

View File

@ -5,6 +5,7 @@ import math
from typing import List from typing import List
from copy import deepcopy from copy import deepcopy
from unittest.mock import MagicMock from unittest.mock import MagicMock
from arrow import Arrow
import pandas as pd import pandas as pd
from freqtrade import optimize from freqtrade import optimize
from freqtrade.optimize.backtesting import Backtesting, start, setup_configuration from freqtrade.optimize.backtesting import Backtesting, start, setup_configuration
@ -255,6 +256,25 @@ def test_backtesting_init(default_conf) -> None:
assert callable(backtesting.populate_sell_trend) assert callable(backtesting.populate_sell_trend)
def test_tickerdata_to_dataframe(default_conf) -> None:
"""
Test Backtesting.tickerdata_to_dataframe() method
"""
timerange = ((None, 'line'), None, -100)
tick = optimize.load_tickerdata_file(None, 'BTC_UNITEST', 1, timerange=timerange)
tickerlist = {'BTC_UNITEST': tick}
backtesting = _BACKTESTING
data = backtesting.tickerdata_to_dataframe(tickerlist)
assert len(data['BTC_UNITEST']) == 100
# Load Analyze to compare the result between Backtesting function and Analyze are the same
analyze = Analyze(default_conf)
data2 = analyze.tickerdata_to_dataframe(tickerlist)
assert data['BTC_UNITEST'].equals(data2['BTC_UNITEST'])
def test_get_timeframe() -> None: def test_get_timeframe() -> None:
""" """
Test Backtesting.get_timeframe() method Test Backtesting.get_timeframe() method
@ -308,8 +328,18 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None:
""" """
Test Backtesting.start() method Test Backtesting.start() method
""" """
mocker.patch.multiple('freqtrade.optimize', load_data=mocked_load_data) def get_timeframe(input1, input2):
mocker.patch('freqtrade.exchange.get_ticker_history', MagicMock) return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
mocker.patch('freqtrade.freqtradebot.Analyze', MagicMock())
mocker.patch('freqtrade.optimize.load_data', mocked_load_data)
mocker.patch('freqtrade.exchange.get_ticker_history')
mocker.patch.multiple(
'freqtrade.optimize.backtesting.Backtesting',
backtest=MagicMock(),
_generate_text_table=MagicMock(return_value='1'),
get_timeframe=get_timeframe,
)
conf = deepcopy(default_conf) conf = deepcopy(default_conf)
conf['exchange']['pair_whitelist'] = ['BTC_UNITEST'] conf['exchange']['pair_whitelist'] = ['BTC_UNITEST']

View File

@ -1,117 +1,108 @@
# pragma pylint: disable=missing-docstring,W0212,C0103 # pragma pylint: disable=missing-docstring,W0212,C0103
import logging import logging
import os
import pytest
from copy import deepcopy
from freqtrade.optimize.hyperopt import calculate_loss, TARGET_TRADES, EXPECTED_MAX_PROFIT, start, \ #from freqtrade.optimize.hyperopt import EXPECTED_MAX_PROFIT, start, \
log_results, save_trials, read_trials, generate_roi_table # log_results, save_trials, read_trials, generate_roi_table
from unittest.mock import MagicMock
from freqtrade.optimize.hyperopt import Hyperopt, start
import freqtrade.tests.conftest as tt # test tools
def test_loss_calculation_prefer_correct_trade_count(): # Avoid to reinit the same object again and again
correct = calculate_loss(1, TARGET_TRADES, 20) _HYPEROPT = Hyperopt(tt.default_conf())
over = calculate_loss(1, TARGET_TRADES + 100, 20)
under = calculate_loss(1, TARGET_TRADES - 100, 20)
assert over > correct
assert under > correct
def test_loss_calculation_prefer_shorter_trades(): # Functions for recurrent object patching
shorter = calculate_loss(1, 100, 20) def create_trials(mocker) -> None:
longer = calculate_loss(1, 100, 30)
assert shorter < longer
def test_loss_calculation_has_limited_profit():
correct = calculate_loss(EXPECTED_MAX_PROFIT, TARGET_TRADES, 20)
over = calculate_loss(EXPECTED_MAX_PROFIT * 2, TARGET_TRADES, 20)
under = calculate_loss(EXPECTED_MAX_PROFIT / 2, TARGET_TRADES, 20)
assert over == correct
assert under > correct
def create_trials(mocker):
""" """
When creating trials, mock the hyperopt Trials so that *by default* When creating trials, mock the hyperopt Trials so that *by default*
- we don't create any pickle'd files in the filesystem - we don't create any pickle'd files in the filesystem
- we might have a pickle'd file so make sure that we return - we might have a pickle'd file so make sure that we return
false when looking for it false when looking for it
""" """
mocker.patch('freqtrade.optimize.hyperopt.TRIALS_FILE', _HYPEROPT.trials_file = os.path.join('freqtrade', 'tests', 'optimize','ut_trials.pickle')
return_value='freqtrade/tests/optimize/ut_trials.pickle')
mocker.patch('freqtrade.optimize.hyperopt.os.path.exists', mocker.patch('freqtrade.optimize.hyperopt.os.path.exists', return_value=False)
return_value=False) mocker.patch('freqtrade.optimize.hyperopt.os.remove', return_value=True)
mocker.patch('freqtrade.optimize.hyperopt.save_trials', mocker.patch('freqtrade.optimize.hyperopt.pickle.dump', return_value=None)
return_value=None)
mocker.patch('freqtrade.optimize.hyperopt.read_trials',
return_value=None)
mocker.patch('freqtrade.optimize.hyperopt.os.remove',
return_value=True)
return mocker.Mock( return mocker.Mock(
results=[{ results=[
{
'loss': 1, 'loss': 1,
'result': 'foo', 'result': 'foo',
'status': 'ok' 'status': 'ok'
}], }
],
best_trial={'misc': {'vals': {'adx': 999}}} best_trial={'misc': {'vals': {'adx': 999}}}
) )
def test_start_calls_fmin(mocker): # Unit tests
trials = create_trials(mocker) def test_loss_calculation_prefer_correct_trade_count() -> None:
mocker.patch('freqtrade.optimize.tickerdata_to_dataframe') """
mocker.patch('freqtrade.optimize.hyperopt.TRIALS', return_value=trials) Test Hyperopt.calculate_loss()
mocker.patch('freqtrade.optimize.hyperopt.sorted', """
return_value=trials.results) hyperopt = _HYPEROPT
mocker.patch('freqtrade.optimize.preprocess')
mocker.patch('freqtrade.optimize.load_data')
mock_fmin = mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
args = mocker.Mock(epochs=1, config='config.json.example', mongodb=False, correct = hyperopt.calculate_loss(1, hyperopt.target_trades, 20)
timerange=None) over = hyperopt.calculate_loss(1, hyperopt.target_trades + 100, 20)
start(args) under = hyperopt.calculate_loss(1, hyperopt.target_trades - 100, 20)
assert over > correct
mock_fmin.assert_called_once() assert under > correct
def test_start_uses_mongotrials(mocker): def test_loss_calculation_prefer_shorter_trades() -> None:
mock_mongotrials = mocker.patch('freqtrade.optimize.hyperopt.MongoTrials', """
return_value=create_trials(mocker)) Test Hyperopt.calculate_loss()
mocker.patch('freqtrade.optimize.tickerdata_to_dataframe') """
mocker.patch('freqtrade.optimize.load_data') hyperopt = _HYPEROPT
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
args = mocker.Mock(epochs=1, config='config.json.example', mongodb=True, shorter = hyperopt.calculate_loss(1, 100, 20)
timerange=None) longer = hyperopt.calculate_loss(1, 100, 30)
start(args) assert shorter < longer
mock_mongotrials.assert_called_once()
def test_log_results_if_loss_improves(mocker): def test_loss_calculation_has_limited_profit() -> None:
logger = mocker.patch('freqtrade.optimize.hyperopt.logger.info') hyperopt = _HYPEROPT
global CURRENT_BEST_LOSS
CURRENT_BEST_LOSS = 2 correct = hyperopt.calculate_loss(hyperopt.expected_max_profit, hyperopt.target_trades, 20)
log_results({ over = hyperopt.calculate_loss(hyperopt.expected_max_profit * 2, hyperopt.target_trades, 20)
under = hyperopt.calculate_loss(hyperopt.expected_max_profit / 2, hyperopt.target_trades, 20)
assert over == correct
assert under > correct
def test_log_results_if_loss_improves(caplog) -> None:
hyperopt = _HYPEROPT
hyperopt.current_best_loss = 2
hyperopt.log_results(
{
'loss': 1, 'loss': 1,
'current_tries': 1, 'current_tries': 1,
'total_tries': 2, 'total_tries': 2,
'result': 'foo' 'result': 'foo'
}) }
)
logger.assert_called_once() assert tt.log_has(' 1/2: foo. Loss 1.00000', caplog.record_tuples)
def test_no_log_if_loss_does_not_improve(mocker): def test_no_log_if_loss_does_not_improve(caplog) -> None:
logger = mocker.patch('freqtrade.optimize.hyperopt.logger.info') hyperopt = _HYPEROPT
global CURRENT_BEST_LOSS hyperopt.current_best_loss = 2
CURRENT_BEST_LOSS = 2 hyperopt.log_results(
log_results({ {
'loss': 3, 'loss': 3,
}) }
)
assert not logger.called assert caplog.record_tuples == []
def test_fmin_best_results(mocker, caplog): def test_fmin_best_results(mocker, default_conf, caplog) -> None:
caplog.set_level(logging.INFO)
fmin_result = { fmin_result = {
"macd_below_zero": 0, "macd_below_zero": 0,
"adx": 1, "adx": 1,
@ -136,38 +127,65 @@ def test_fmin_best_results(mocker, caplog):
"roi_p3": 3, "roi_p3": 3,
} }
mocker.patch('freqtrade.optimize.hyperopt.MongoTrials', return_value=create_trials(mocker)) conf = deepcopy(default_conf)
mocker.patch('freqtrade.optimize.tickerdata_to_dataframe') conf.update({'config': 'config.json.example'})
mocker.patch('freqtrade.optimize.load_data') conf.update({'epochs': 1})
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value=fmin_result) conf.update({'timerange': None})
args = mocker.Mock(epochs=1, config='config.json.example', mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
timerange=None) mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value=fmin_result)
start(args) mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf)
mocker.patch('freqtrade.logger.Logger.set_format', MagicMock())
hyperopt = Hyperopt(conf)
hyperopt.trials = create_trials(mocker)
hyperopt.tickerdata_to_dataframe = MagicMock()
hyperopt.start()
exists = [ exists = [
'Best parameters', 'Best parameters:',
'"adx": {\n "enabled": true,\n "value": 15.0\n },', '"adx": {\n "enabled": true,\n "value": 15.0\n },',
'"fastd": {\n "enabled": true,\n "value": 40.0\n },',
'"green_candle": {\n "enabled": true\n },', '"green_candle": {\n "enabled": true\n },',
'"macd_below_zero": {\n "enabled": false\n },',
'"mfi": {\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 },', '"trigger": {\n "type": "faststoch10"\n },',
'"stoploss": -0.1', '"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: for line in exists:
assert line in caplog.text assert line in caplog.text
def test_fmin_throw_value_error(mocker, caplog): def test_fmin_throw_value_error(mocker, default_conf, caplog) -> None:
caplog.set_level(logging.INFO) mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
mocker.patch('freqtrade.optimize.hyperopt.MongoTrials', return_value=create_trials(mocker))
mocker.patch('freqtrade.optimize.tickerdata_to_dataframe')
mocker.patch('freqtrade.optimize.load_data')
mocker.patch('freqtrade.optimize.hyperopt.fmin', side_effect=ValueError()) mocker.patch('freqtrade.optimize.hyperopt.fmin', side_effect=ValueError())
args = mocker.Mock(epochs=1, config='config.json.example', conf = deepcopy(default_conf)
timerange=None) conf.update({'config': 'config.json.example'})
start(args) conf.update({'epochs': 1})
conf.update({'timerange': None})
mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf)
mocker.patch('freqtrade.logger.Logger.set_format', MagicMock())
hyperopt = Hyperopt(conf)
hyperopt.trials = create_trials(mocker)
hyperopt.tickerdata_to_dataframe = MagicMock()
hyperopt.start()
exists = [ exists = [
'Best Result:', 'Best Result:',
@ -179,68 +197,80 @@ def test_fmin_throw_value_error(mocker, caplog):
assert line in caplog.text assert line in caplog.text
def test_resuming_previous_hyperopt_results_succeeds(mocker): def test_resuming_previous_hyperopt_results_succeeds(mocker, default_conf) -> None:
import freqtrade.optimize.hyperopt as hyperopt
trials = create_trials(mocker) trials = create_trials(mocker)
mocker.patch('freqtrade.optimize.hyperopt.TRIALS',
return_value=trials)
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.read_trials',
return_value=trials)
mock_save = mocker.patch('freqtrade.optimize.hyperopt.save_trials',
return_value=None)
mocker.patch('freqtrade.optimize.hyperopt.sorted',
return_value=trials.results)
mocker.patch('freqtrade.optimize.preprocess')
mocker.patch('freqtrade.optimize.load_data')
mocker.patch('freqtrade.optimize.hyperopt.fmin',
return_value={})
args = mocker.Mock(epochs=1,
config='config.json.example',
mongodb=False,
timerange=None)
start(args) conf = deepcopy(default_conf)
conf.update({'config': 'config.json.example'})
conf.update({'epochs': 1})
conf.update({'mongodb': False})
conf.update({'timerange': None})
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={})
mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf)
mocker.patch('freqtrade.logger.Logger.set_format', MagicMock())
hyperopt = Hyperopt(conf)
hyperopt.trials = trials
hyperopt.tickerdata_to_dataframe = MagicMock()
hyperopt.start()
mock_read.assert_called_once() mock_read.assert_called_once()
mock_save.assert_called_once() mock_save.assert_called_once()
current_tries = hyperopt._CURRENT_TRIES current_tries = hyperopt.current_tries
total_tries = hyperopt.TOTAL_TRIES total_tries = hyperopt.total_tries
assert current_tries == len(trials.results) assert current_tries == len(trials.results)
assert total_tries == (current_tries + len(trials.results)) assert total_tries == (current_tries + len(trials.results))
def test_save_trials_saves_trials(mocker): def test_save_trials_saves_trials(mocker, caplog) -> None:
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.save_trials()
assert tt.log_has(
'Saving Trials to \'freqtrade/tests/optimize/ut_trials.pickle\'',
caplog.record_tuples
)
mock_dump.assert_called_once()
def test_read_trials_returns_trials_file(mocker, default_conf, caplog) -> None:
trials = create_trials(mocker) trials = create_trials(mocker)
mock_dump = mocker.patch('freqtrade.optimize.hyperopt.pickle.dump', mock_load = mocker.patch('freqtrade.optimize.hyperopt.pickle.load', return_value=trials)
return_value=None) mock_open = mocker.patch('freqtrade.optimize.hyperopt.open', return_value=mock_load)
trials_path = mocker.patch('freqtrade.optimize.hyperopt.TRIALS_FILE',
return_value='ut_trials.pickle')
mocker.patch('freqtrade.optimize.hyperopt.open',
return_value=trials_path)
save_trials(trials, trials_path)
mock_dump.assert_called_once_with(trials, trials_path) hyperopt = _HYPEROPT
hyperopt_trial = hyperopt.read_trials()
assert tt.log_has(
def test_read_trials_returns_trials_file(mocker): 'Reading Trials from \'freqtrade/tests/optimize/ut_trials.pickle\'',
trials = create_trials(mocker) caplog.record_tuples
mock_load = mocker.patch('freqtrade.optimize.hyperopt.pickle.load', )
return_value=trials) assert hyperopt_trial == trials
mock_open = mocker.patch('freqtrade.optimize.hyperopt.open',
return_value=mock_load)
assert read_trials() == trials
mock_open.assert_called_once() mock_open.assert_called_once()
mock_load.assert_called_once() mock_load.assert_called_once()
def test_roi_table_generation(): def test_roi_table_generation() -> None:
params = { params = {
'roi_t1': 5, 'roi_t1': 5,
'roi_t2': 10, 'roi_t2': 10,
@ -249,4 +279,49 @@ def test_roi_table_generation():
'roi_p2': 2, 'roi_p2': 2,
'roi_p3': 3, 'roi_p3': 3,
} }
assert generate_roi_table(params) == {'0': 6, '15': 3, '25': 1, '30': 0}
hyperopt = _HYPEROPT
assert hyperopt.generate_roi_table(params) == {'0': 6, '15': 3, '25': 1, '30': 0}
def test_start_calls_fmin(mocker, default_conf) -> None:
trials = create_trials(mocker)
mocker.patch('freqtrade.optimize.hyperopt.sorted', return_value=trials.results)
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
mock_fmin = mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
conf = deepcopy(default_conf)
conf.update({'config': 'config.json.example'})
conf.update({'epochs': 1})
conf.update({'mongodb': False})
conf.update({'timerange': None})
hyperopt = Hyperopt(conf)
hyperopt.trials = trials
hyperopt.tickerdata_to_dataframe = MagicMock()
hyperopt.start()
mock_fmin.assert_called_once()
def test_start_uses_mongotrials(mocker, default_conf) -> None:
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
mock_fmin = mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
mock_mongotrials = mocker.patch(
'freqtrade.optimize.hyperopt.MongoTrials',
return_value=create_trials(mocker)
)
conf = deepcopy(default_conf)
conf.update({'config': 'config.json.example'})
conf.update({'epochs': 1})
conf.update({'mongodb': True})
conf.update({'timerange': None})
mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf)
hyperopt = Hyperopt(conf)
hyperopt.tickerdata_to_dataframe = MagicMock()
hyperopt.start()
mock_mongotrials.assert_called_once()
mock_fmin.assert_called_once()

View File

@ -6,7 +6,6 @@ import logging
import uuid import uuid
from shutil import copyfile from shutil import copyfile
from freqtrade import optimize from freqtrade import optimize
from freqtrade.analyze import Analyze
from freqtrade.optimize.__init__ import make_testdata_path, download_pairs,\ from freqtrade.optimize.__init__ import make_testdata_path, download_pairs,\
download_backtesting_testdata, load_tickerdata_file, trim_tickerlist download_backtesting_testdata, load_tickerdata_file, trim_tickerlist
from freqtrade.misc import file_dump_json from freqtrade.misc import file_dump_json
@ -220,16 +219,6 @@ def test_init(default_conf, mocker) -> None:
) )
def test_tickerdata_to_dataframe(default_conf) -> None:
analyze = Analyze(default_conf)
timerange = ((None, 'line'), None, -100)
tick = load_tickerdata_file(None, 'BTC_UNITEST', 1, timerange=timerange)
tickerlist = {'BTC_UNITEST': tick}
data = analyze.tickerdata_to_dataframe(tickerlist)
assert len(data['BTC_UNITEST']) == 100
def test_trim_tickerlist() -> None: def test_trim_tickerlist() -> None:
with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file: with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file:
ticker_list = json.load(data_file) ticker_list = json.load(data_file)

View File

@ -10,8 +10,9 @@ import logging
import arrow import arrow
from pandas import DataFrame from pandas import DataFrame
import freqtrade.tests.conftest as tt # test tools
from freqtrade.analyze import Analyze, SignalType from freqtrade.analyze import Analyze, SignalType
from freqtrade.optimize.__init__ import load_tickerdata_file
import freqtrade.tests.conftest as tt # test tools
# Avoid to reinit the same object again and again # Avoid to reinit the same object again and again
@ -173,3 +174,16 @@ def test_parse_ticker_dataframe(ticker_history, ticker_history_without_bv):
# Test file without BV data # Test file without BV data
dataframe = Analyze.parse_ticker_dataframe(ticker_history_without_bv) dataframe = Analyze.parse_ticker_dataframe(ticker_history_without_bv)
assert dataframe.columns.tolist() == columns assert dataframe.columns.tolist() == columns
def test_tickerdata_to_dataframe(default_conf) -> None:
"""
Test Analyze.tickerdata_to_dataframe() method
"""
analyze = Analyze(default_conf)
timerange = ((None, 'line'), None, -100)
tick = load_tickerdata_file(None, 'BTC_UNITEST', 1, timerange=timerange)
tickerlist = {'BTC_UNITEST': tick}
data = analyze.tickerdata_to_dataframe(tickerlist)
assert len(data['BTC_UNITEST']) == 100

View File

@ -1,17 +1,19 @@
# pragma pylint: disable=missing-docstring, C0103 # pragma pylint: disable=missing-docstring, C0103
import pandas import pandas
import freqtrade.optimize from freqtrade.optimize import load_data
from freqtrade import analyze from freqtrade.analyze import Analyze, SignalType
_pairs = ['BTC_ETH'] _pairs = ['BTC_ETH']
def load_dataframe_pair(pairs): def load_dataframe_pair(pairs):
ld = freqtrade.optimize.load_data(None, ticker_interval=5, pairs=pairs) ld = load_data(None, ticker_interval=5, pairs=pairs)
assert isinstance(ld, dict) assert isinstance(ld, dict)
assert isinstance(pairs[0], str) assert isinstance(pairs[0], str)
dataframe = ld[pairs[0]] dataframe = ld[pairs[0]]
analyze = Analyze({'strategy': 'default_strategy'})
dataframe = analyze.analyze_ticker(dataframe) dataframe = analyze.analyze_ticker(dataframe)
return dataframe return dataframe