Move Backtesting to a class and add unit tests

This commit is contained in:
Gerald Lonlas 2018-02-08 23:35:38 -08:00
parent db67b10605
commit 1d251d6151
9 changed files with 942 additions and 427 deletions

View File

@ -42,6 +42,9 @@ class Configuration(object):
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
config = self._load_backtesting_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]:
@ -59,6 +62,51 @@ class Configuration(object):
return self._validate_config(conf) return self._validate_config(conf)
def _load_backtesting_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
"""
Extract information for sys.argv and load Backtesting and Hyperopt configuration
:return: configuration as dictionary
"""
# If -i/--ticker-interval is used we override the configuration parameter
# (that will override the strategy configuration)
if 'ticker_interval' in self.args and self.args.ticker_interval:
config.update({'ticker_interval': self.args.ticker_interval})
self.logger.info('Parameter -i/--ticker-interval detected ...')
self.logger.info('Using ticker_interval: %d ...', config.get('ticker_interval'))
# If -l/--live is used we add it to the configuration
if 'live' in self.args and self.args.live:
config.update({'live': True})
self.logger.info('Parameter -l/--live detected ...')
# If --realistic-simulation is used we add it to the configuration
if 'realistic_simulation' in self.args and self.args.realistic_simulation:
config.update({'realistic_simulation': True})
self.logger.info('Parameter --realistic-simulation detected ...')
self.logger.info('Using max_open_trades: %s ...', config.get('max_open_trades'))
# If --timerange is used we add it to the configuration
if 'timerange' in self.args and self.args.timerange:
config.update({'timerange': self.args.timerange})
self.logger.info('Parameter --timerange detected: %s ...', self.args.timerange)
# If --datadir is used we add it to the configuration
if 'datadir' in self.args and self.args.datadir:
config.update({'datadir': self.args.datadir})
self.logger.info('Parameter --datadir detected: %s ...', self.args.datadir)
# If -r/--refresh-pairs-cached is used we add it to the configuration
if 'refresh_pairs' in self.args and self.args.refresh_pairs:
config.update({'refresh_pairs': True})
self.logger.info('Parameter -r/--refresh-pairs-cached detected ...')
# If --export is used we add it to the configuration
if 'export' in self.args and self.args.export:
config.update({'export': self.args.export})
self.logger.info('Parameter --export detected: %s ...', self.args.export)
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

@ -20,21 +20,61 @@ class Logger(object):
""" """
self.name = name self.name = name
self.level = level self.level = level
self.logger = None
self._init_logger() self._init_logger()
def _init_logger(self) -> logging: def _init_logger(self) -> None:
""" """
Setup the bot logger configuration Setup the bot logger configuration
:return: logging object :return: None
""" """
logging.basicConfig( logging.basicConfig(
level=self.level, level=self.level,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
) )
self.logger = self.get_logger()
self.set_level(self.level)
def get_logger(self) -> logging.RootLogger: def get_logger(self) -> logging.RootLogger:
""" """
Return the logger instance to use for sending message Return the logger instance to use for sending message
:return: the logger instance :return: the logger instance
""" """
return logging.getLogger(self.name) return logging.getLogger(self.name)
def set_name(self, name: str) -> logging.RootLogger:
"""
Set the name of the logger
:param name: Name of the logger
:return: None
"""
self.name = name
self.logger = self.get_logger()
return self.logger
def set_level(self, level) -> None:
"""
Set the level of the logger
:param level:
:return: None
"""
self.level = level
self.logger.setLevel(self.level)
def set_format(self, log_format: str, propagate: bool = False) -> None:
"""
Set a new logging format
:return: None
"""
handler = logging.StreamHandler()
len_handlers = len(self.logger.handlers)
if len_handlers:
self.logger.removeHandler(handler)
handler.setFormatter(logging.Formatter(log_format))
self.logger.addHandler(handler)
self.logger.propagate = propagate

View File

@ -1,18 +1,16 @@
# pragma pylint: disable=missing-docstring # pragma pylint: disable=missing-docstring
import logging
import json import json
import os import os
from typing import Optional, List, Dict from typing import Optional, List, Dict
from pandas import DataFrame import gzip
from freqtrade.exchange import get_ticker_history from freqtrade.exchange import get_ticker_history
from freqtrade.analyze import populate_indicators, parse_ticker_dataframe
from freqtrade import misc from freqtrade import misc
from freqtrade.logger import Logger
from user_data.hyperopt_conf import hyperopt_optimize_conf from user_data.hyperopt_conf import hyperopt_optimize_conf
import gzip
logger = logging.getLogger(__name__) logger = Logger(name=__name__).get_logger()
def trim_tickerlist(tickerlist, timerange): def trim_tickerlist(tickerlist, timerange):
@ -84,21 +82,13 @@ def load_data(datadir: str, ticker_interval: int, pairs: Optional[List[str]] = N
return result return result
def tickerdata_to_dataframe(data):
preprocessed = preprocess(data)
return preprocessed
def preprocess(tickerdata: Dict[str, List]) -> Dict[str, DataFrame]:
"""Creates a dataframe and populates indicators for given ticker data"""
return {pair: populate_indicators(parse_ticker_dataframe(pair_data))
for pair, pair_data in tickerdata.items()}
def make_testdata_path(datadir: str) -> str: def make_testdata_path(datadir: str) -> str:
"""Return the path where testdata files are stored""" """Return the path where testdata files are stored"""
return datadir or os.path.abspath(os.path.join(os.path.dirname(__file__), return datadir or os.path.abspath(
'..', 'tests', 'testdata')) os.path.join(
os.path.dirname(__file__), '..', 'tests', 'testdata'
)
)
def download_pairs(datadir, pairs: List[str], ticker_interval: int) -> bool: def download_pairs(datadir, pairs: List[str], ticker_interval: int) -> bool:
@ -115,11 +105,6 @@ def download_pairs(datadir, pairs: List[str], ticker_interval: int) -> bool:
return True return True
def file_dump_json(filename, data):
with open(filename, "wt") as fp:
json.dump(data, fp)
# FIX: 20180110, suggest rename interval to tick_interval # FIX: 20180110, suggest rename interval to tick_interval
def download_backtesting_testdata(datadir: str, pair: str, interval: int = 5) -> bool: def download_backtesting_testdata(datadir: str, pair: str, interval: int = 5) -> bool:
""" """
@ -142,8 +127,8 @@ def download_backtesting_testdata(datadir: str, pair: str, interval: int = 5) ->
)) ))
if os.path.isfile(filename): if os.path.isfile(filename):
with open(filename, "rt") as fp: with open(filename, "rt") as file:
data = json.load(fp) data = json.load(file)
logger.debug("Current Start: {}".format(data[1]['T'])) logger.debug("Current Start: {}".format(data[1]['T']))
logger.debug("Current End: {}".format(data[-1:][0]['T'])) logger.debug("Current End: {}".format(data[-1:][0]['T']))
else: else:

View File

@ -1,235 +1,321 @@
# pragma pylint: disable=missing-docstring,W0212 # pragma pylint: disable=missing-docstring, W0212, too-many-arguments
"""
This module contains the backtesting logic
"""
from typing import Dict, Tuple, Any
import logging import logging
from typing import Dict, Tuple
import arrow import arrow
from pandas import DataFrame, Series from pandas import DataFrame, Series
from tabulate import tabulate from tabulate import tabulate
import freqtrade.misc as misc
import freqtrade.optimize as optimize import freqtrade.optimize as optimize
from freqtrade import exchange from freqtrade.arguments import Arguments
from freqtrade.analyze import populate_buy_trend, populate_sell_trend
from freqtrade.exchange import Bittrex from freqtrade.exchange import Bittrex
from freqtrade.main import should_sell from freqtrade.configuration import Configuration
from freqtrade import exchange
from freqtrade.analyze import Analyze
from freqtrade.logger import Logger
from freqtrade.misc import file_dump_json
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.strategy.strategy import Strategy
logger = logging.getLogger(__name__)
def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]: class Backtesting(object):
""" """
Get the maximum timeframe for the given backtest data Backtesting class, this class contains all the logic to run a backtest
:param data: dictionary with preprocessed backtesting data
:return: tuple containing min_date, max_date
"""
all_dates = Series([])
for pair_data in data.values():
all_dates = all_dates.append(pair_data['date'])
all_dates.sort_values(inplace=True)
return arrow.get(all_dates.iloc[0]), arrow.get(all_dates.iloc[-1])
To run a backtest:
backtesting = Backtesting(config)
backtesting.start()
"""
def __init__(self, config: Dict[str, Any]) -> None:
self.logging = Logger(name=__name__)
self.logger = self.logging.get_logger()
def generate_text_table( self.config = config
data: Dict[str, Dict], results: DataFrame, stake_currency, ticker_interval) -> str: self.analyze = None
""" self.ticker_interval = None
Generates and returns a text table for the given backtest data and the results dataframe self.tickerdata_to_dataframe = None
:return: pretty printed table with tabulate as str self.populate_buy_trend = None
""" self.populate_sell_trend = None
floatfmt = ('s', 'd', '.2f', '.8f', '.1f') self._init()
tabular_data = []
headers = ['pair', 'buy count', 'avg profit %', def _init(self) -> None:
'total profit ' + stake_currency, 'avg duration', 'profit', 'loss'] """
for pair in data: Init objects required for backtesting
result = results[results.currency == pair] :return: None
"""
self.analyze = Analyze(self.config)
self.ticker_interval = self.analyze.strategy.ticker_interval
self.tickerdata_to_dataframe = self.analyze.tickerdata_to_dataframe
self.populate_buy_trend = self.analyze.populate_buy_trend
self.populate_sell_trend = self.analyze.populate_sell_trend
exchange._API = Bittrex({'key': '', 'secret': ''})
@staticmethod
def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]:
"""
Get the maximum timeframe for the given backtest data
:param data: dictionary with preprocessed backtesting data
:return: tuple containing min_date, max_date
"""
all_dates = Series([])
for pair_data in data.values():
all_dates = all_dates.append(pair_data['date'])
all_dates.sort_values(inplace=True)
return arrow.get(all_dates.iloc[0]), arrow.get(all_dates.iloc[-1])
def _generate_text_table(self, data: Dict[str, Dict], results: DataFrame) -> str:
"""
Generates and returns a text table for the given backtest data and the results dataframe
:return: pretty printed table with tabulate as str
"""
stake_currency = self.config.get('stake_currency')
ticker_interval = self.ticker_interval
floatfmt = ('s', 'd', '.2f', '.8f', '.1f')
tabular_data = []
headers = ['pair', 'buy count', 'avg profit %',
'total profit ' + stake_currency, 'avg duration', 'profit', 'loss']
for pair in data:
result = results[results.currency == pair]
tabular_data.append([
pair,
len(result.index),
result.profit_percent.mean() * 100.0,
result.profit_BTC.sum(),
result.duration.mean() * ticker_interval,
len(result[result.profit_BTC > 0]),
len(result[result.profit_BTC < 0])
])
# Append Total
tabular_data.append([ tabular_data.append([
pair, 'TOTAL',
len(result.index), len(results.index),
result.profit_percent.mean() * 100.0, results.profit_percent.mean() * 100.0,
result.profit_BTC.sum(), results.profit_BTC.sum(),
result.duration.mean() * ticker_interval, results.duration.mean() * ticker_interval,
len(result[result.profit_BTC > 0]), len(results[results.profit_BTC > 0]),
len(result[result.profit_BTC < 0]) len(results[results.profit_BTC < 0])
]) ])
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt)
# Append Total def _get_sell_trade_entry(self, pair, row, buy_subset, ticker, trade_count_lock, args):
tabular_data.append([ stake_amount = args['stake_amount']
'TOTAL', max_open_trades = args.get('max_open_trades', 0)
len(results.index), trade = Trade(
results.profit_percent.mean() * 100.0, open_rate=row.close,
results.profit_BTC.sum(), open_date=row.date,
results.duration.mean() * ticker_interval, stake_amount=stake_amount,
len(results[results.profit_BTC > 0]), amount=stake_amount / row.open,
len(results[results.profit_BTC < 0]) fee=exchange.get_fee()
]) )
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt)
# calculate win/lose forwards from buy point
def get_sell_trade_entry(pair, row, buy_subset, ticker, trade_count_lock, args): sell_subset = ticker[ticker.date > row.date][['close', 'date', 'sell']]
stake_amount = args['stake_amount'] for row2 in sell_subset.itertuples(index=True):
max_open_trades = args.get('max_open_trades', 0)
trade = Trade(open_rate=row.close,
open_date=row.date,
stake_amount=stake_amount,
amount=stake_amount / row.open,
fee=exchange.get_fee()
)
# calculate win/lose forwards from buy point
sell_subset = ticker[ticker.date > row.date][['close', 'date', 'sell']]
for row2 in sell_subset.itertuples(index=True):
if max_open_trades > 0:
# Increase trade_count_lock for every iteration
trade_count_lock[row2.date] = trade_count_lock.get(row2.date, 0) + 1
# Buy is on is in the buy_subset there is a row that matches the date
# of the sell event
buy_signal = not buy_subset[buy_subset.date == row2.date].empty
if(should_sell(trade, row2.close, row2.date, buy_signal, row2.sell)):
return row2, (pair,
trade.calc_profit_percent(rate=row2.close),
trade.calc_profit(rate=row2.close),
row2.Index - row.Index
), row2.date
return None
def backtest(args) -> DataFrame:
"""
Implements backtesting functionality
:param args: a dict containing:
stake_amount: btc amount to use for each trade
processed: a processed dictionary with format {pair, data}
max_open_trades: maximum number of concurrent trades (default: 0, disabled)
realistic: do we try to simulate realistic trades? (default: True)
sell_profit_only: sell if profit only
use_sell_signal: act on sell-signal
stoploss: use stoploss
:return: DataFrame
"""
processed = args['processed']
max_open_trades = args.get('max_open_trades', 0)
realistic = args.get('realistic', True)
record = args.get('record', None)
records = []
trades = []
trade_count_lock: dict = {}
exchange._API = Bittrex({'key': '', 'secret': ''})
for pair, pair_data in processed.items():
pair_data['buy'], pair_data['sell'] = 0, 0
ticker = populate_sell_trend(populate_buy_trend(pair_data))
# for each buy point
lock_pair_until = None
headers = ['buy', 'open', 'close', 'date', 'sell']
buy_subset = ticker[(ticker.buy == 1) & (ticker.sell == 0)][headers]
for row in buy_subset.itertuples(index=True):
if realistic:
if lock_pair_until is not None and row.date <= lock_pair_until:
continue
if max_open_trades > 0: if max_open_trades > 0:
# Check if max_open_trades has already been reached for the given date # Increase trade_count_lock for every iteration
if not trade_count_lock.get(row.date, 0) < max_open_trades: trade_count_lock[row2.date] = trade_count_lock.get(row2.date, 0) + 1
continue
if max_open_trades > 0: # Buy is on is in the buy_subset there is a row that matches the date
# Increase lock # of the sell event
trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1 buy_signal = not buy_subset[buy_subset.date == row2.date].empty
if(
self.analyze.should_sell(
trade=trade,
rate=row2.close,
date=row2.date,
buy=buy_signal,
sell=row2.sell
)
):
return \
row2, \
(
pair,
trade.calc_profit_percent(rate=row2.close),
trade.calc_profit(rate=row2.close),
row2.Index - row.Index
),\
row2.date
return None
ret = get_sell_trade_entry(pair, row, buy_subset, ticker, def backtest(self, args) -> DataFrame:
trade_count_lock, args) """
if ret: Implements backtesting functionality
row2, trade_entry, next_date = ret
lock_pair_until = next_date NOTE: This method is used by Hyperopt at each iteration. Please keep it optimized.
trades.append(trade_entry) Of course try to not have ugly code. By some accessor are sometime slower than functions.
if record: Avoid, logging on this method
# Note, need to be json.dump friendly
# record a tuple of pair, current_profit_percent, :param args: a dict containing:
# entry-date, duration stake_amount: btc amount to use for each trade
records.append((pair, trade_entry[1], processed: a processed dictionary with format {pair, data}
row.date.strftime('%s'), max_open_trades: maximum number of concurrent trades (default: 0, disabled)
row2.date.strftime('%s'), realistic: do we try to simulate realistic trades? (default: True)
row.Index, trade_entry[3])) sell_profit_only: sell if profit only
# For now export inside backtest(), maybe change so that backtest() use_sell_signal: act on sell-signal
# returns a tuple like: (dataframe, records, logs, etc) stoploss: use stoploss
if record and record.find('trades') >= 0: :return: DataFrame
logger.info('Dumping backtest results') """
misc.file_dump_json('backtest-result.json', records) processed = args['processed']
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration'] max_open_trades = args.get('max_open_trades', 0)
return DataFrame.from_records(trades, columns=labels) realistic = args.get('realistic', True)
record = args.get('record', None)
records = []
trades = []
trade_count_lock = {}
for pair, pair_data in processed.items():
pair_data['buy'], pair_data['sell'] = 0, 0
ticker = self.populate_sell_trend(
self.populate_buy_trend(pair_data)
)
# for each buy point
lock_pair_until = None
headers = ['buy', 'open', 'close', 'date', 'sell']
buy_subset = ticker[(ticker.buy == 1) & (ticker.sell == 0)][headers]
for row in buy_subset.itertuples(index=True):
if realistic:
if lock_pair_until is not None and row.date <= lock_pair_until:
continue
if max_open_trades > 0:
# Check if max_open_trades has already been reached for the given date
if not trade_count_lock.get(row.date, 0) < max_open_trades:
continue
if max_open_trades > 0:
# Increase lock
trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1
ret = self._get_sell_trade_entry(
pair=pair,
row=row,
buy_subset=buy_subset,
ticker=ticker,
trade_count_lock=trade_count_lock,
args=args
)
if ret:
row2, trade_entry, next_date = ret
lock_pair_until = next_date
trades.append(trade_entry)
if record:
# Note, need to be json.dump friendly
# record a tuple of pair, current_profit_percent,
# entry-date, duration
records.append((pair, trade_entry[1],
row.date.strftime('%s'),
row2.date.strftime('%s'),
row.Index, trade_entry[3]))
# For now export inside backtest(), maybe change so that backtest()
# returns a tuple like: (dataframe, records, logs, etc)
if record and record.find('trades') >= 0:
self.logger.info('Dumping backtest results')
file_dump_json('backtest-result.json', records)
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration']
return DataFrame.from_records(trades, columns=labels)
def start(self) -> None:
"""
Run a backtesting end-to-end
:return: None
"""
data = {}
pairs = self.config['exchange']['pair_whitelist']
if self.config.get('live'):
self.logger.info('Downloading data for all pairs in whitelist ...')
for pair in pairs:
data[pair] = exchange.get_ticker_history(pair, self.ticker_interval)
else:
self.logger.info('Using local backtesting data (using whitelist in given config) ...')
self.logger.info('Using stake_currency: %s ...', self.config['stake_currency'])
self.logger.info('Using stake_amount: %s ...', self.config['stake_amount'])
timerange = Arguments.parse_timerange(self.config.get('timerange'))
data = optimize.load_data(
self.config['datadir'],
pairs=pairs,
ticker_interval=self.ticker_interval,
refresh_pairs=self.config.get('refresh_pairs', False),
timerange=timerange
)
max_open_trades = self.config.get('max_open_trades', 0)
preprocessed = self.tickerdata_to_dataframe(data)
# Print timeframe
min_date, max_date = self.get_timeframe(preprocessed)
self.logger.info(
'Measuring data from %s up to %s (%s days)..',
min_date.isoformat(),
max_date.isoformat(),
(max_date - min_date).days
)
# Execute backtest and print results
sell_profit_only = self.config.get('experimental', {}).get('sell_profit_only', False)
use_sell_signal = self.config.get('experimental', {}).get('use_sell_signal', False)
results = self.backtest(
{
'stake_amount': self.config.get('stake_amount'),
'processed': preprocessed,
'max_open_trades': max_open_trades,
'realistic': self.config.get('realistic_simulation', False),
'sell_profit_only': sell_profit_only,
'use_sell_signal': use_sell_signal,
'stoploss': self.analyze.strategy.stoploss,
'record': self.config.get('export')
}
)
self.logging.set_format('%(message)s')
self.logger.info(
'\n==================================== '
'BACKTESTING REPORT'
' ====================================\n'
'%s',
self._generate_text_table(
data,
results
)
)
def start(args): def setup_configuration(args) -> Dict[str, Any]:
"""
Prepare the configuration for the backtesting
:param args: Cli args from Arguments()
:return: Configuration
"""
configuration = Configuration(args)
config = configuration.get_config()
# Ensure we do not use Exchange credentials
config['exchange']['key'] = ''
config['exchange']['secret'] = ''
return config
def start(args) -> None:
"""
Start Backtesting script
:param args: Cli args from Arguments()
:return: None
"""
# Initialize logger # Initialize logger
logging.basicConfig( logger = Logger(name=__name__).get_logger()
level=args.loglevel, logger.info('Starting freqtrade in Backtesting mode')
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
)
exchange._API = Bittrex({'key': '', 'secret': ''}) # Initialize configuration
config = setup_configuration(args)
logger.info('Using config: %s ...', args.config) # Initialize backtesting object
config = misc.load_config(args.config) backtesting = Backtesting(config)
backtesting.start()
# If -i/--ticker-interval is use we override the configuration parameter
# (that will override the strategy configuration)
if args.ticker_interval:
config.update({'ticker_interval': args.ticker_interval})
# init the strategy to use
config.update({'strategy': args.strategy})
strategy = Strategy()
strategy.init(config)
logger.info('Using ticker_interval: %d ...', strategy.ticker_interval)
data = {}
pairs = config['exchange']['pair_whitelist']
if args.live:
logger.info('Downloading data for all pairs in whitelist ...')
for pair in pairs:
data[pair] = exchange.get_ticker_history(pair, strategy.ticker_interval)
else:
logger.info('Using local backtesting data (using whitelist in given config) ...')
logger.info('Using stake_currency: %s ...', config['stake_currency'])
logger.info('Using stake_amount: %s ...', config['stake_amount'])
timerange = misc.parse_timerange(args.timerange)
data = optimize.load_data(args.datadir,
pairs=pairs,
ticker_interval=strategy.ticker_interval,
refresh_pairs=args.refresh_pairs,
timerange=timerange)
max_open_trades = 0
if args.realistic_simulation:
logger.info('Using max_open_trades: %s ...', config['max_open_trades'])
max_open_trades = config['max_open_trades']
# Monkey patch config
from freqtrade import main
main._CONF = config
preprocessed = optimize.tickerdata_to_dataframe(data)
# Print timeframe
min_date, max_date = get_timeframe(preprocessed)
logger.info('Measuring data from %s up to %s (%s days)..',
min_date.isoformat(),
max_date.isoformat(),
(max_date-min_date).days)
# Execute backtest and print results
sell_profit_only = config.get('experimental', {}).get('sell_profit_only', False)
use_sell_signal = config.get('experimental', {}).get('use_sell_signal', False)
results = backtest({'stake_amount': config['stake_amount'],
'processed': preprocessed,
'max_open_trades': max_open_trades,
'realistic': args.realistic_simulation,
'sell_profit_only': sell_profit_only,
'use_sell_signal': use_sell_signal,
'stoploss': strategy.stoploss,
'record': args.export
})
logger.info(
'\n==================================== BACKTESTING REPORT ====================================\n%s', # noqa
generate_text_table(data, results, config['stake_currency'], strategy.ticker_interval)
)

View File

@ -269,13 +269,6 @@ def result():
return Analyze.parse_ticker_dataframe(json.load(data_file)) return Analyze.parse_ticker_dataframe(json.load(data_file))
@pytest.fixture
def default_strategy():
strategy = Strategy()
strategy.init({'strategy': 'default_strategy'})
return strategy
# FIX: # FIX:
# Create an fixture/function # Create an fixture/function
# that inserts a trade of some type and open-status # that inserts a trade of some type and open-status

View File

@ -1,14 +1,24 @@
# pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103 # pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, unused-argument
import logging import json
import math import math
from typing import List
from copy import deepcopy
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pandas as pd import pandas as pd
from freqtrade import exchange, optimize from freqtrade import optimize
from freqtrade.exchange import Bittrex from freqtrade.optimize.backtesting import Backtesting, start, setup_configuration
from freqtrade.optimize import preprocess from freqtrade.arguments import Arguments
from freqtrade.optimize.backtesting import backtest, generate_text_table, get_timeframe from freqtrade.analyze import Analyze
import freqtrade.optimize.backtesting as backtesting import freqtrade.tests.conftest as tt # test tools
# Avoid to reinit the same object again and again
_BACKTESTING = Backtesting(tt.default_conf())
def get_args(args) -> List[str]:
return Arguments(args, '').get_parsed_arg()
def trim_dictlist(dict_list, num): def trim_dictlist(dict_list, num):
@ -18,60 +28,6 @@ def trim_dictlist(dict_list, num):
return new return new
def test_generate_text_table():
results = pd.DataFrame(
{
'currency': ['BTC_ETH', 'BTC_ETH'],
'profit_percent': [0.1, 0.2],
'profit_BTC': [0.2, 0.4],
'duration': [10, 30],
'profit': [2, 0],
'loss': [0, 0]
}
)
print(generate_text_table({'BTC_ETH': {}}, results, 'BTC', 5))
assert generate_text_table({'BTC_ETH': {}}, results, 'BTC', 5) == (
'pair buy count avg profit % total profit BTC avg duration profit loss\n' # noqa
'------- ----------- -------------- ------------------ -------------- -------- ------\n' # noqa
'BTC_ETH 2 15.00 0.60000000 100.0 2 0\n' # noqa
'TOTAL 2 15.00 0.60000000 100.0 2 0') # noqa
def test_get_timeframe(default_strategy):
data = preprocess(optimize.load_data(
None, ticker_interval=1, pairs=['BTC_UNITEST']))
min_date, max_date = get_timeframe(data)
assert min_date.isoformat() == '2017-11-04T23:02:00+00:00'
assert max_date.isoformat() == '2017-11-14T22:59:00+00:00'
def test_backtest(default_strategy, default_conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
data = optimize.load_data(None, ticker_interval=5, pairs=['BTC_ETH'])
data = trim_dictlist(data, -200)
results = backtest({'stake_amount': default_conf['stake_amount'],
'processed': optimize.preprocess(data),
'max_open_trades': 10,
'realistic': True})
assert not results.empty
def test_backtest_1min_ticker_interval(default_strategy, default_conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
# Run a backtesting for an exiting 5min ticker_interval
data = optimize.load_data(None, ticker_interval=1, pairs=['BTC_UNITEST'])
data = trim_dictlist(data, -200)
results = backtest({'stake_amount': default_conf['stake_amount'],
'processed': optimize.preprocess(data),
'max_open_trades': 1,
'realistic': True})
assert not results.empty
def load_data_test(what): def load_data_test(what):
timerange = ((None, 'line'), None, -100) timerange = ((None, 'line'), None, -100)
data = optimize.load_data(None, ticker_interval=1, pairs=['BTC_UNITEST'], timerange=timerange) data = optimize.load_data(None, ticker_interval=1, pairs=['BTC_UNITEST'], timerange=timerange)
@ -115,79 +71,324 @@ def load_data_test(what):
return data return data
def simple_backtest(config, contour, num_results): def simple_backtest(config, contour, num_results) -> None:
backtesting = _BACKTESTING
data = load_data_test(contour) data = load_data_test(contour)
processed = optimize.preprocess(data) processed = backtesting.tickerdata_to_dataframe(data)
assert isinstance(processed, dict) assert isinstance(processed, dict)
results = backtest({'stake_amount': config['stake_amount'], results = backtesting.backtest(
'processed': processed, {
'max_open_trades': 1, 'stake_amount': config['stake_amount'],
'realistic': True}) 'processed': processed,
'max_open_trades': 1,
'realistic': True
}
)
# results :: <class 'pandas.core.frame.DataFrame'> # results :: <class 'pandas.core.frame.DataFrame'>
assert len(results) == num_results assert len(results) == num_results
# Test backtest on offline data
# loaded by freqdata/optimize/__init__.py::load_data()
def test_backtest2(default_conf, mocker, default_strategy):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
data = optimize.load_data(None, ticker_interval=5, pairs=['BTC_ETH'])
data = trim_dictlist(data, -200)
results = backtest({'stake_amount': default_conf['stake_amount'],
'processed': optimize.preprocess(data),
'max_open_trades': 10,
'realistic': True})
assert not results.empty
def test_processed(default_conf, mocker, default_strategy):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
dict_of_tickerrows = load_data_test('raise')
dataframes = optimize.preprocess(dict_of_tickerrows)
dataframe = dataframes['BTC_UNITEST']
cols = dataframe.columns
# assert the dataframe got some of the indicator columns
for col in ['close', 'high', 'low', 'open', 'date',
'ema50', 'ao', 'macd', 'plus_dm']:
assert col in cols
def test_backtest_pricecontours(default_conf, mocker, default_strategy):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
tests = [['raise', 17], ['lower', 0], ['sine', 17]]
for [contour, numres] in tests:
simple_backtest(default_conf, contour, numres)
def mocked_load_data(datadir, pairs=[], ticker_interval=0, refresh_pairs=False, timerange=None): def mocked_load_data(datadir, pairs=[], ticker_interval=0, refresh_pairs=False, timerange=None):
tickerdata = optimize.load_tickerdata_file(datadir, 'BTC_UNITEST', 1, timerange=timerange) tickerdata = optimize.load_tickerdata_file(datadir, 'BTC_UNITEST', 1, timerange=timerange)
pairdata = {'BTC_UNITEST': tickerdata} pairdata = {'BTC_UNITEST': tickerdata}
return pairdata return pairdata
def test_backtest_start(default_conf, mocker, caplog): # Unit tests
caplog.set_level(logging.INFO) def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
default_conf['exchange']['pair_whitelist'] = ['BTC_UNITEST'] """
mocker.patch.dict('freqtrade.main._CONF', default_conf) Test setup_configuration() function
mocker.patch('freqtrade.misc.load_config', new=lambda s: default_conf) """
mocker.patch.multiple('freqtrade.optimize', mocker.patch('freqtrade.configuration.open', mocker.mock_open(
load_data=mocked_load_data) read_data=json.dumps(default_conf)
args = MagicMock() ))
args.ticker_interval = 1
args.level = 10 args = [
args.live = False '--config', 'config.json',
args.datadir = None '--strategy', 'default_strategy',
args.export = None 'backtesting'
args.timerange = '-100' # needed due to MagicMock malleability ]
backtesting.start(args)
config = setup_configuration(get_args(args))
assert 'max_open_trades' in config
assert 'stake_currency' in config
assert 'stake_amount' in config
assert 'exchange' in config
assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config
assert tt.log_has(
'Parameter --datadir detected: {} ...'.format(config['datadir']),
caplog.record_tuples
)
assert 'ticker_interval' in config
assert not tt.log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples)
assert 'live' not in config
assert not tt.log_has('Parameter -l/--live detected ...', caplog.record_tuples)
assert 'realistic_simulation' not in config
assert not tt.log_has('Parameter --realistic-simulation detected ...', caplog.record_tuples)
assert 'refresh_pairs' not in config
assert not tt.log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
assert 'timerange' not in config
assert 'export' not in config
def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None:
"""
Test setup_configuration() function
"""
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf)
))
args = [
'--config', 'config.json',
'--strategy', 'default_strategy',
'--datadir', '/foo/bar',
'backtesting',
'--ticker-interval', '1',
'--live',
'--realistic-simulation',
'--refresh-pairs-cached',
'--timerange', ':100',
'--export', '/bar/foo'
]
config = setup_configuration(get_args(args))
assert 'max_open_trades' in config
assert 'stake_currency' in config
assert 'stake_amount' in config
assert 'exchange' in config
assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config
assert tt.log_has(
'Parameter --datadir detected: {} ...'.format(config['datadir']),
caplog.record_tuples
)
assert 'ticker_interval' in config
assert tt.log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples)
assert tt.log_has(
'Using ticker_interval: 1 ...',
caplog.record_tuples
)
assert 'live' in config
assert tt.log_has('Parameter -l/--live detected ...', caplog.record_tuples)
assert 'realistic_simulation'in config
assert tt.log_has('Parameter --realistic-simulation detected ...', caplog.record_tuples)
assert tt.log_has('Using max_open_trades: 1 ...', caplog.record_tuples)
assert 'refresh_pairs'in config
assert tt.log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
import pprint
pprint.pprint(caplog.record_tuples)
pprint.pprint(config['timerange'])
assert 'timerange' in config
assert tt.log_has(
'Parameter --timerange detected: {} ...'.format(config['timerange']),
caplog.record_tuples
)
assert 'export' in config
assert tt.log_has(
'Parameter --export detected: {} ...'.format(config['export']),
caplog.record_tuples
)
def test_start(mocker, default_conf, caplog) -> None:
"""
Test start() function
"""
start_mock = MagicMock()
mocker.patch('freqtrade.optimize.backtesting.Backtesting.start', start_mock)
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf)
))
args = [
'--config', 'config.json',
'--strategy', 'default_strategy',
'backtesting'
]
args = get_args(args)
start(args)
assert tt.log_has(
'Starting freqtrade in Backtesting mode',
caplog.record_tuples
)
assert start_mock.call_count == 1
def test_backtesting__init__(mocker, default_conf) -> None:
"""
Test Backtesting.__init__() method
"""
init_mock = MagicMock()
mocker.patch('freqtrade.optimize.backtesting.Backtesting._init', init_mock)
backtesting = Backtesting(default_conf)
assert backtesting.config == default_conf
assert backtesting.analyze is None
assert backtesting.ticker_interval is None
assert backtesting.tickerdata_to_dataframe is None
assert backtesting.populate_buy_trend is None
assert backtesting.populate_sell_trend is None
assert init_mock.call_count == 1
def test_backtesting_init(default_conf) -> None:
"""
Test Backtesting._init() method
"""
backtesting = Backtesting(default_conf)
assert backtesting.config == default_conf
assert isinstance(backtesting.analyze, Analyze)
assert backtesting.ticker_interval == 5
assert callable(backtesting.tickerdata_to_dataframe)
assert callable(backtesting.populate_buy_trend)
assert callable(backtesting.populate_sell_trend)
def test_get_timeframe() -> None:
"""
Test Backtesting.get_timeframe() method
"""
backtesting = _BACKTESTING
data = backtesting.tickerdata_to_dataframe(
optimize.load_data(
None,
ticker_interval=1,
pairs=['BTC_UNITEST']
)
)
min_date, max_date = backtesting.get_timeframe(data)
assert min_date.isoformat() == '2017-11-04T23:02:00+00:00'
assert max_date.isoformat() == '2017-11-14T22:59:00+00:00'
def test_generate_text_table():
"""
Test Backtesting.generate_text_table() method
"""
backtesting = _BACKTESTING
results = pd.DataFrame(
{
'currency': ['BTC_ETH', 'BTC_ETH'],
'profit_percent': [0.1, 0.2],
'profit_BTC': [0.2, 0.4],
'duration': [10, 30],
'profit': [2, 0],
'loss': [0, 0]
}
)
result_str = (
'pair buy count avg profit % '
'total profit BTC avg duration profit loss\n'
'------- ----------- -------------- '
'------------------ -------------- -------- ------\n'
'BTC_ETH 2 15.00 '
'0.60000000 100.0 2 0\n'
'TOTAL 2 15.00 '
'0.60000000 100.0 2 0'
)
assert backtesting._generate_text_table(data={'BTC_ETH': {}}, results=results) == result_str
def test_backtesting_start(default_conf, mocker, caplog) -> None:
"""
Test Backtesting.start() method
"""
mocker.patch.multiple('freqtrade.optimize', load_data=mocked_load_data)
mocker.patch('freqtrade.exchange.get_ticker_history', MagicMock)
conf = deepcopy(default_conf)
conf['exchange']['pair_whitelist'] = ['BTC_UNITEST']
conf['ticker_interval'] = 1
conf['live'] = False
conf['datadir'] = None
conf['export'] = None
conf['timerange'] = '-100'
backtesting = Backtesting(conf)
backtesting.start()
# check the logs, that will contain the backtest result # check the logs, that will contain the backtest result
exists = ['Using max_open_trades: 1 ...', exists = [
'Using stake_amount: 0.001 ...', 'Using local backtesting data (using whitelist in given config) ...',
'Measuring data from 2017-11-14T21:17:00+00:00 ' 'Using stake_currency: BTC ...',
'up to 2017-11-14T22:59:00+00:00 (0 days)..'] 'Using stake_amount: 0.001 ...',
'Measuring data from 2017-11-14T21:17:00+00:00 '
'up to 2017-11-14T22:59:00+00:00 (0 days)..'
]
for line in exists: for line in exists:
assert ('freqtrade.optimize.backtesting', assert tt.log_has(line, caplog.record_tuples)
logging.INFO,
line) in caplog.record_tuples
def test_backtest(default_conf) -> None:
"""
Test Backtesting.backtest() method
"""
backtesting = _BACKTESTING
data = optimize.load_data(None, ticker_interval=5, pairs=['BTC_ETH'])
data = trim_dictlist(data, -200)
results = backtesting.backtest(
{
'stake_amount': default_conf['stake_amount'],
'processed': backtesting.tickerdata_to_dataframe(data),
'max_open_trades': 10,
'realistic': True
}
)
assert not results.empty
def test_backtest_1min_ticker_interval(default_conf) -> None:
"""
Test Backtesting.backtest() method with 1 min ticker
"""
backtesting = _BACKTESTING
# Run a backtesting for an exiting 5min ticker_interval
data = optimize.load_data(None, ticker_interval=1, pairs=['BTC_UNITEST'])
data = trim_dictlist(data, -200)
results = backtesting.backtest(
{
'stake_amount': default_conf['stake_amount'],
'processed': backtesting.tickerdata_to_dataframe(data),
'max_open_trades': 1,
'realistic': True
}
)
assert not results.empty
def test_processed() -> None:
"""
Test Backtesting.backtest() method with offline data
"""
backtesting = _BACKTESTING
dict_of_tickerrows = load_data_test('raise')
dataframes = backtesting.tickerdata_to_dataframe(dict_of_tickerrows)
dataframe = dataframes['BTC_UNITEST']
cols = dataframe.columns
# assert the dataframe got some of the indicator columns
for col in ['close', 'high', 'low', 'open', 'date',
'ema50', 'ao', 'macd', 'plus_dm']:
assert col in cols
def test_backtest_pricecontours(default_conf) -> None:
tests = [['raise', 17], ['lower', 0], ['sine', 17]]
for [contour, numres] in tests:
simple_backtest(default_conf, contour, numres)

View File

@ -5,10 +5,11 @@ import json
import logging import logging
import uuid import uuid
from shutil import copyfile from shutil import copyfile
from freqtrade import exchange, optimize from freqtrade import optimize
from freqtrade.exchange import Bittrex 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, file_dump_json download_backtesting_testdata, load_tickerdata_file, trim_tickerlist
from freqtrade.misc import file_dump_json
# Change this if modifying BTC_UNITEST testdatafile # Change this if modifying BTC_UNITEST testdatafile
_BTC_UNITTEST_LENGTH = 13681 _BTC_UNITTEST_LENGTH = 13681
@ -45,12 +46,11 @@ def _clean_test_file(file: str) -> None:
os.rename(file_swp, file) os.rename(file_swp, file)
def test_load_data_30min_ticker(default_conf, ticker_history, mocker, caplog): def test_load_data_30min_ticker(ticker_history, mocker, caplog) -> None:
caplog.set_level(logging.INFO) """
Test load_data() with 30 min ticker
"""
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history) mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
file = 'freqtrade/tests/testdata/BTC_UNITTEST-30.json' file = 'freqtrade/tests/testdata/BTC_UNITTEST-30.json'
_backup_file(file, copy_file=True) _backup_file(file, copy_file=True)
@ -62,12 +62,11 @@ def test_load_data_30min_ticker(default_conf, ticker_history, mocker, caplog):
_clean_test_file(file) _clean_test_file(file)
def test_load_data_5min_ticker(default_conf, ticker_history, mocker, caplog): def test_load_data_5min_ticker(ticker_history, mocker, caplog) -> None:
caplog.set_level(logging.INFO) """
Test load_data() with 5 min ticker
"""
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history) mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
file = 'freqtrade/tests/testdata/BTC_ETH-5.json' file = 'freqtrade/tests/testdata/BTC_ETH-5.json'
_backup_file(file, copy_file=True) _backup_file(file, copy_file=True)
@ -79,12 +78,11 @@ def test_load_data_5min_ticker(default_conf, ticker_history, mocker, caplog):
_clean_test_file(file) _clean_test_file(file)
def test_load_data_1min_ticker(default_conf, ticker_history, mocker, caplog): def test_load_data_1min_ticker(ticker_history, mocker, caplog) -> None:
caplog.set_level(logging.INFO) """
Test load_data() with 1 min ticker
"""
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history) mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
file = 'freqtrade/tests/testdata/BTC_ETH-1.json' file = 'freqtrade/tests/testdata/BTC_ETH-1.json'
_backup_file(file, copy_file=True) _backup_file(file, copy_file=True)
@ -96,12 +94,11 @@ def test_load_data_1min_ticker(default_conf, ticker_history, mocker, caplog):
_clean_test_file(file) _clean_test_file(file)
def test_load_data_with_new_pair_1min(default_conf, ticker_history, mocker, caplog): def test_load_data_with_new_pair_1min(ticker_history, mocker, caplog) -> None:
caplog.set_level(logging.INFO) """
Test load_data() with 1 min ticker
"""
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history) mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
file = 'freqtrade/tests/testdata/BTC_MEME-1.json' file = 'freqtrade/tests/testdata/BTC_MEME-1.json'
_backup_file(file) _backup_file(file)
@ -113,14 +110,12 @@ def test_load_data_with_new_pair_1min(default_conf, ticker_history, mocker, capl
_clean_test_file(file) _clean_test_file(file)
def test_testdata_path(): def test_testdata_path() -> None:
assert os.path.join('freqtrade', 'tests', 'testdata') in make_testdata_path(None) assert os.path.join('freqtrade', 'tests', 'testdata') in make_testdata_path(None)
def test_download_pairs(default_conf, ticker_history, mocker): def test_download_pairs(ticker_history, mocker) -> None:
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history) mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history)
mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
file1_1 = 'freqtrade/tests/testdata/BTC_MEME-1.json' file1_1 = 'freqtrade/tests/testdata/BTC_MEME-1.json'
file1_5 = 'freqtrade/tests/testdata/BTC_MEME-5.json' file1_5 = 'freqtrade/tests/testdata/BTC_MEME-5.json'
@ -157,13 +152,10 @@ def test_download_pairs(default_conf, ticker_history, mocker):
_clean_test_file(file2_5) _clean_test_file(file2_5)
def test_download_pairs_exception(default_conf, ticker_history, mocker, caplog): def test_download_pairs_exception(ticker_history, mocker, caplog) -> None:
caplog.set_level(logging.INFO)
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history) mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history)
mocker.patch('freqtrade.optimize.__init__.download_backtesting_testdata', mocker.patch('freqtrade.optimize.__init__.download_backtesting_testdata',
side_effect=BaseException('File Error')) side_effect=BaseException('File Error'))
mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
file1_1 = 'freqtrade/tests/testdata/BTC_MEME-1.json' file1_1 = 'freqtrade/tests/testdata/BTC_MEME-1.json'
file1_5 = 'freqtrade/tests/testdata/BTC_MEME-5.json' file1_5 = 'freqtrade/tests/testdata/BTC_MEME-5.json'
@ -179,10 +171,8 @@ def test_download_pairs_exception(default_conf, ticker_history, mocker, caplog):
'Failed to download the pair: "BTC-MEME", Interval: 1 min') in caplog.record_tuples 'Failed to download the pair: "BTC-MEME", Interval: 1 min') in caplog.record_tuples
def test_download_backtesting_testdata(default_conf, ticker_history, mocker): def test_download_backtesting_testdata(ticker_history, mocker) -> None:
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history) mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history)
mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
# Download a 1 min ticker file # Download a 1 min ticker file
file1 = 'freqtrade/tests/testdata/BTC_XEL-1.json' file1 = 'freqtrade/tests/testdata/BTC_XEL-1.json'
@ -200,7 +190,7 @@ def test_download_backtesting_testdata(default_conf, ticker_history, mocker):
_clean_test_file(file2) _clean_test_file(file2)
def test_download_backtesting_testdata2(mocker): def test_download_backtesting_testdata2(mocker) -> None:
tick = [{'T': 'bar'}, {'T': 'foo'}] tick = [{'T': 'bar'}, {'T': 'foo'}]
mocker.patch('freqtrade.misc.file_dump_json', return_value=None) mocker.patch('freqtrade.misc.file_dump_json', return_value=None)
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=tick) mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=tick)
@ -208,7 +198,7 @@ def test_download_backtesting_testdata2(mocker):
assert download_backtesting_testdata(None, pair="BTC-UNITEST", interval=3) assert download_backtesting_testdata(None, pair="BTC-UNITEST", interval=3)
def test_load_tickerdata_file(): def test_load_tickerdata_file() -> None:
# 7 does not exist in either format. # 7 does not exist in either format.
assert not load_tickerdata_file(None, 'BTC_UNITEST', 7) assert not load_tickerdata_file(None, 'BTC_UNITEST', 7)
# 1 exists only as a .json # 1 exists only as a .json
@ -219,22 +209,28 @@ def test_load_tickerdata_file():
assert _BTC_UNITTEST_LENGTH == len(tickerdata) assert _BTC_UNITTEST_LENGTH == len(tickerdata)
def test_init(default_conf, mocker): def test_init(default_conf, mocker) -> None:
conf = {'exchange': {'pair_whitelist': []}} conf = {'exchange': {'pair_whitelist': []}}
mocker.patch('freqtrade.optimize.hyperopt_optimize_conf', return_value=conf) mocker.patch('freqtrade.optimize.hyperopt_optimize_conf', return_value=conf)
assert {} == optimize.load_data('', pairs=[], refresh_pairs=True, assert {} == optimize.load_data(
ticker_interval=int(default_conf['ticker_interval'])) '',
pairs=[],
refresh_pairs=True,
ticker_interval=int(default_conf['ticker_interval'])
)
def test_tickerdata_to_dataframe(): def test_tickerdata_to_dataframe(default_conf) -> None:
analyze = Analyze(default_conf)
timerange = ((None, 'line'), None, -100) timerange = ((None, 'line'), None, -100)
tick = load_tickerdata_file(None, 'BTC_UNITEST', 1, timerange=timerange) tick = load_tickerdata_file(None, 'BTC_UNITEST', 1, timerange=timerange)
tickerlist = {'BTC_UNITEST': tick} tickerlist = {'BTC_UNITEST': tick}
data = optimize.tickerdata_to_dataframe(tickerlist) data = analyze.tickerdata_to_dataframe(tickerlist)
assert len(data['BTC_UNITEST']) == 100 assert len(data['BTC_UNITEST']) == 100
def test_trim_tickerlist(): 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)
ticker_list_len = len(ticker_list) ticker_list_len = len(ticker_list)
@ -279,8 +275,11 @@ def test_trim_tickerlist():
assert ticker_list_len == ticker_len assert ticker_list_len == ticker_len
def test_file_dump_json(): def test_file_dump_json() -> None:
"""
Test file_dump_json()
:return: None
"""
file = 'freqtrade/tests/testdata/test_{id}.json'.format(id=str(uuid.uuid4())) file = 'freqtrade/tests/testdata/test_{id}.json'.format(id=str(uuid.uuid4()))
data = {'bar': 'foo'} data = {'bar': 'foo'}

View File

@ -1,5 +1,4 @@
# pragma pylint: disable=protected-access, invalid-name, missing-docstring # pragma pylint: disable=protected-access, invalid-name
""" """
Unit test file for configuration.py Unit test file for configuration.py
""" """
@ -18,7 +17,6 @@ import freqtrade.tests.conftest as tt # test tools
def test_configuration_object() -> None: def test_configuration_object() -> None:
""" """
Test the Constants object has the mandatory Constants Test the Constants object has the mandatory Constants
:return: None
""" """
assert hasattr(Configuration, '_load_config') assert hasattr(Configuration, '_load_config')
assert hasattr(Configuration, '_load_config_file') assert hasattr(Configuration, '_load_config_file')
@ -30,12 +28,11 @@ def test_configuration_object() -> None:
def test_load_config_invalid_pair(default_conf, mocker) -> None: def test_load_config_invalid_pair(default_conf, mocker) -> None:
""" """
Test the configuration validator with an invalid PAIR format Test the configuration validator with an invalid PAIR format
:param default_conf: Configuration already read from a file (JSON format)
:return: None
""" """
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.configuration.Configuration', 'freqtrade.configuration.Configuration',
_load_config=MagicMock(return_value=[]) _load_config=MagicMock(return_value=[]),
show_info=MagicMock
) )
conf = deepcopy(default_conf) conf = deepcopy(default_conf)
conf['exchange']['pair_whitelist'].append('BTC-ETH') conf['exchange']['pair_whitelist'].append('BTC-ETH')
@ -48,12 +45,11 @@ def test_load_config_invalid_pair(default_conf, mocker) -> None:
def test_load_config_missing_attributes(default_conf, mocker) -> None: def test_load_config_missing_attributes(default_conf, mocker) -> None:
""" """
Test the configuration validator with a missing attribute Test the configuration validator with a missing attribute
:param default_conf: Configuration already read from a file (JSON format)
:return: None
""" """
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.configuration.Configuration', 'freqtrade.configuration.Configuration',
_load_config=MagicMock(return_value=[]) _load_config=MagicMock(return_value=[]),
show_info=MagicMock
) )
conf = deepcopy(default_conf) conf = deepcopy(default_conf)
conf.pop('exchange') conf.pop('exchange')
@ -65,12 +61,12 @@ def test_load_config_missing_attributes(default_conf, mocker) -> None:
def test_load_config_file(default_conf, mocker, caplog) -> None: def test_load_config_file(default_conf, mocker, caplog) -> None:
""" """
Test _load_config_file() method Test Configuration._load_config_file() method
:return:
""" """
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.configuration.Configuration', 'freqtrade.configuration.Configuration',
_load_config=MagicMock(return_value=[]) _load_config=MagicMock(return_value=[]),
show_info=MagicMock
) )
file_mock = mocker.patch('freqtrade.configuration.open', mocker.mock_open( file_mock = mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf) read_data=json.dumps(default_conf)
@ -85,6 +81,9 @@ def test_load_config_file(default_conf, mocker, caplog) -> None:
def test_load_config(default_conf, mocker) -> None: def test_load_config(default_conf, mocker) -> None:
"""
Test Configuration._load_config() without any cli params
"""
mocker.patch('freqtrade.configuration.open', mocker.mock_open( mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf) read_data=json.dumps(default_conf)
)) ))
@ -100,6 +99,9 @@ def test_load_config(default_conf, mocker) -> None:
def test_load_config_with_params(default_conf, mocker) -> None: def test_load_config_with_params(default_conf, mocker) -> None:
"""
Test Configuration._load_config() with cli params used
"""
mocker.patch('freqtrade.configuration.open', mocker.mock_open( mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf) read_data=json.dumps(default_conf)
)) ))
@ -123,6 +125,9 @@ def test_load_config_with_params(default_conf, mocker) -> None:
def test_show_info(default_conf, mocker, caplog) -> None: def test_show_info(default_conf, mocker, caplog) -> None:
"""
Test Configuration.show_info()
"""
mocker.patch('freqtrade.configuration.open', mocker.mock_open( mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf) read_data=json.dumps(default_conf)
)) ))
@ -155,3 +160,118 @@ def test_show_info(default_conf, mocker, caplog) -> None:
'Dry run is disabled. (--dry_run_db ignored)', 'Dry run is disabled. (--dry_run_db ignored)',
caplog.record_tuples caplog.record_tuples
) )
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
"""
Test setup_configuration() function
"""
mocker.patch('freqtrade.configuration.Configuration.show_info', MagicMock)
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf)
))
args = [
'--config', 'config.json',
'--strategy', 'default_strategy',
'backtesting'
]
args = Arguments(args, '').get_parsed_arg()
configuration = Configuration(args)
config = configuration.get_config()
assert 'max_open_trades' in config
assert 'stake_currency' in config
assert 'stake_amount' in config
assert 'exchange' in config
assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config
assert tt.log_has(
'Parameter --datadir detected: {} ...'.format(config['datadir']),
caplog.record_tuples
)
assert 'ticker_interval' in config
assert not tt.log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples)
assert 'live' not in config
assert not tt.log_has('Parameter -l/--live detected ...', caplog.record_tuples)
assert 'realistic_simulation' not in config
assert not tt.log_has('Parameter --realistic-simulation detected ...', caplog.record_tuples)
assert 'refresh_pairs' not in config
assert not tt.log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
assert 'timerange' not in config
assert 'export' not in config
def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None:
"""
Test setup_configuration() function
"""
mocker.patch('freqtrade.configuration.Configuration.show_info', MagicMock)
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf)
))
args = [
'--config', 'config.json',
'--strategy', 'default_strategy',
'--datadir', '/foo/bar',
'backtesting',
'--ticker-interval', '1',
'--live',
'--realistic-simulation',
'--refresh-pairs-cached',
'--timerange', ':100',
'--export', '/bar/foo'
]
args = Arguments(args, '').get_parsed_arg()
configuration = Configuration(args)
config = configuration.get_config()
assert 'max_open_trades' in config
assert 'stake_currency' in config
assert 'stake_amount' in config
assert 'exchange' in config
assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config
assert tt.log_has(
'Parameter --datadir detected: {} ...'.format(config['datadir']),
caplog.record_tuples
)
assert 'ticker_interval' in config
assert tt.log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples)
assert tt.log_has(
'Using ticker_interval: 1 ...',
caplog.record_tuples
)
assert 'live' in config
assert tt.log_has('Parameter -l/--live detected ...', caplog.record_tuples)
assert 'realistic_simulation'in config
assert tt.log_has('Parameter --realistic-simulation detected ...', caplog.record_tuples)
assert tt.log_has('Using max_open_trades: 1 ...', caplog.record_tuples)
assert 'refresh_pairs'in config
assert tt.log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
import pprint
pprint.pprint(caplog.record_tuples)
pprint.pprint(config['timerange'])
assert 'timerange' in config
assert tt.log_has(
'Parameter --timerange detected: {} ...'.format(config['timerange']),
caplog.record_tuples
)
assert 'export' in config
assert tt.log_has(
'Parameter --export detected: {} ...'.format(config['export']),
caplog.record_tuples
)

View File

@ -26,11 +26,12 @@ def test_logger_object() -> None:
def test_get_logger() -> None: def test_get_logger() -> None:
""" """
Test logger.get_logger() Test Logger.get_logger() and Logger._init_logger()
:return: None :return: None
""" """
logger = Logger(name='Foo', level=logging.WARNING) logger = Logger(name='get_logger', level=logging.WARNING)
get_logger = logger.get_logger() get_logger = logger.get_logger()
assert logger.logger is get_logger
assert get_logger is not None assert get_logger is not None
assert hasattr(get_logger, 'debug') assert hasattr(get_logger, 'debug')
assert hasattr(get_logger, 'info') assert hasattr(get_logger, 'info')
@ -39,15 +40,57 @@ def test_get_logger() -> None:
assert hasattr(get_logger, 'exception') assert hasattr(get_logger, 'exception')
def test_set_name() -> None:
"""
Test Logger.set_name()
:return: None
"""
logger = Logger(name='set_name')
assert logger.name == 'set_name'
logger.set_name('set_name_new')
assert logger.name == 'set_name_new'
def test_set_level() -> None:
"""
Test Logger.set_name()
:return: None
"""
logger = Logger(name='Foo', level=logging.WARNING)
assert logger.level == logging.WARNING
assert logger.get_logger().level == logging.WARNING
logger.set_level(logging.INFO)
assert logger.level == logging.INFO
assert logger.get_logger().level == logging.INFO
def test_sending_msg(caplog) -> None: def test_sending_msg(caplog) -> None:
""" """
Test send a logging message Test send a logging message
:return: None :return: None
""" """
logger = Logger(name='FooBar', level=logging.WARNING).get_logger() logger = Logger(name='sending_msg', level=logging.WARNING).get_logger()
logger.info('I am an INFO message') logger.info('I am an INFO message')
assert('FooBar', logging.INFO, 'I am an INFO message') not in caplog.record_tuples assert('sending_msg', logging.INFO, 'I am an INFO message') not in caplog.record_tuples
logger.warning('I am an WARNING message') logger.warning('I am an WARNING message')
assert ('FooBar', logging.WARNING, 'I am an WARNING message') in caplog.record_tuples assert ('sending_msg', logging.WARNING, 'I am an WARNING message') in caplog.record_tuples
def test_set_format(caplog) -> None:
"""
Test Logger.set_format()
:return: None
"""
log = Logger(name='set_format')
logger = log.get_logger()
logger.info('I am the first message')
assert ('set_format', logging.INFO, 'I am the first message') in caplog.record_tuples
log.set_format(log_format='%(message)s', propagate=True)
logger.info('I am the second message')
assert ('set_format', logging.INFO, 'I am the second message') in caplog.record_tuples