move backtesting to freqtrade.optimize.backtesting

This commit is contained in:
gcarq 2017-11-24 23:58:35 +01:00
parent 858d2329e5
commit 3b37f77a4d
9 changed files with 80 additions and 102 deletions

View File

@ -4,6 +4,7 @@ Functions to analyze ticker data with indicators and produce buy and sell signal
import logging import logging
from datetime import timedelta from datetime import timedelta
from enum import Enum from enum import Enum
from typing import List, Dict
import arrow import arrow
import talib.abstract as ta import talib.abstract as ta
@ -113,18 +114,13 @@ def populate_sell_trend(dataframe: DataFrame) -> DataFrame:
return dataframe return dataframe
def analyze_ticker(pair: str) -> DataFrame: def analyze_ticker(ticker_history: List[Dict]) -> DataFrame:
""" """
Get ticker data for given currency pair, push it to a DataFrame and Parses the given ticker history and returns a populated DataFrame
add several TA indicators and buy signal to it add several TA indicators and buy signal to it
:return DataFrame with ticker data and indicator data :return DataFrame with ticker data and indicator data
""" """
ticker_hist = get_ticker_history(pair) dataframe = parse_ticker_dataframe(ticker_history)
if not ticker_hist:
logger.warning('Empty ticker history for pair %s', pair)
return DataFrame()
dataframe = parse_ticker_dataframe(ticker_hist)
dataframe = populate_indicators(dataframe) dataframe = populate_indicators(dataframe)
dataframe = populate_buy_trend(dataframe) dataframe = populate_buy_trend(dataframe)
dataframe = populate_sell_trend(dataframe) dataframe = populate_sell_trend(dataframe)
@ -137,8 +133,13 @@ def get_signal(pair: str, signal: SignalType) -> bool:
:param pair: pair in format BTC_ANT or BTC-ANT :param pair: pair in format BTC_ANT or BTC-ANT
:return: True if pair is good for buying, False otherwise :return: True if pair is good for buying, False otherwise
""" """
ticker_hist = get_ticker_history(pair)
if not ticker_hist:
logger.warning('Empty ticker history for pair %s', pair)
return False
try: try:
dataframe = analyze_ticker(pair) dataframe = analyze_ticker(ticker_hist)
except ValueError as ex: except ValueError as ex:
logger.warning('Unable to analyze ticker for pair %s: %s', pair, str(ex)) logger.warning('Unable to analyze ticker for pair %s: %s', pair, str(ex))
return False return False

View File

@ -2,7 +2,6 @@ import argparse
import enum import enum
import json import json
import logging import logging
import os
import time import time
from typing import Any, Callable, List, Dict from typing import Any, Callable, List, Dict
@ -129,9 +128,11 @@ def parse_args(args: List[str]):
def build_subcommands(parser: argparse.ArgumentParser) -> None: def build_subcommands(parser: argparse.ArgumentParser) -> None:
""" Builds and attaches all subcommands """ """ Builds and attaches all subcommands """
from freqtrade.optimize import backtesting
subparsers = parser.add_subparsers(dest='subparser') subparsers = parser.add_subparsers(dest='subparser')
backtest = subparsers.add_parser('backtesting', help='backtesting module') backtest = subparsers.add_parser('backtesting', help='backtesting module')
backtest.set_defaults(func=start_backtesting) backtest.set_defaults(func=backtesting.start)
backtest.add_argument( backtest.add_argument(
'-l', '--live', '-l', '--live',
action='store_true', action='store_true',
@ -154,25 +155,6 @@ def build_subcommands(parser: argparse.ArgumentParser) -> None:
) )
def start_backtesting(args) -> None:
"""
Exports all args as environment variables and starts backtesting via pytest.
:param args: arguments namespace
:return:
"""
import pytest
os.environ.update({
'BACKTEST': 'true',
'BACKTEST_LIVE': 'true' if args.live else '',
'BACKTEST_CONFIG': args.config,
'BACKTEST_TICKER_INTERVAL': str(args.ticker_interval),
'BACKTEST_REALISTIC_SIMULATION': 'true' if args.realistic_simulation else '',
})
path = os.path.join(os.path.dirname(__file__), 'tests', 'test_backtesting.py')
pytest.main(['-s', path])
# Required json-schema for user specified config # Required json-schema for user specified config
CONF_SCHEMA = { CONF_SCHEMA = {
'type': 'object', 'type': 'object',

View File

@ -0,0 +1 @@
from . import backtesting

View File

@ -2,11 +2,9 @@
import logging import logging
import os
from typing import Tuple, Dict from typing import Tuple, Dict
import arrow import arrow
import pytest
from pandas import DataFrame from pandas import DataFrame
from tabulate import tabulate from tabulate import tabulate
@ -83,12 +81,12 @@ def generate_text_table(data: Dict[str, Dict], results: DataFrame, stake_currenc
return tabulate(tabular_data, headers=headers) return tabulate(tabular_data, headers=headers)
def backtest(config: Dict, processed, mocker, max_open_trades=0, realistic=True): def backtest(config: Dict, processed: Dict[str, DataFrame],
max_open_trades: int = 0, realistic: bool = True) -> DataFrame:
""" """
Implements backtesting functionality Implements backtesting functionality
:param config: config to use :param config: config to use
:param processed: a processed dictionary with format {pair, data} :param processed: a processed dictionary with format {pair, data}
:param mocker: mocker instance
:param max_open_trades: maximum number of concurrent trades (default: 0, disabled) :param max_open_trades: maximum number of concurrent trades (default: 0, disabled)
:param realistic: do we try to simulate realistic trades? (default: True) :param realistic: do we try to simulate realistic trades? (default: True)
:return: DataFrame :return: DataFrame
@ -96,7 +94,6 @@ def backtest(config: Dict, processed, mocker, max_open_trades=0, realistic=True)
trades = [] trades = []
trade_count_lock = {} trade_count_lock = {}
exchange._API = Bittrex({'key': '', 'secret': ''}) exchange._API = Bittrex({'key': '', 'secret': ''})
mocker.patch.dict('freqtrade.main._CONF', config)
for pair, pair_data in processed.items(): for pair, pair_data in processed.items():
pair_data['buy'], pair_data['sell'] = 0, 0 pair_data['buy'], pair_data['sell'] = 0, 0
ticker = populate_sell_trend(populate_buy_trend(pair_data)) ticker = populate_sell_trend(populate_buy_trend(pair_data))
@ -138,38 +135,23 @@ def backtest(config: Dict, processed, mocker, max_open_trades=0, realistic=True)
return DataFrame.from_records(trades, columns=labels) return DataFrame.from_records(trades, columns=labels)
def get_max_open_trades(config): def start(args):
if not os.environ.get('BACKTEST_REALISTIC_SIMULATION'):
return 0
print('Using max_open_trades: {} ...'.format(config['max_open_trades']))
return config['max_open_trades']
@pytest.mark.skipif(not os.environ.get('BACKTEST'), reason="BACKTEST not set")
def test_backtest(backtest_conf, mocker):
print('') print('')
exchange._API = Bittrex({'key': '', 'secret': ''}) exchange._API = Bittrex({'key': '', 'secret': ''})
# Load configuration file based on env variable print('Using config: {} ...'.format(args.config))
conf_path = os.environ.get('BACKTEST_CONFIG') config = load_config(args.config)
if conf_path:
print('Using config: {} ...'.format(conf_path))
config = load_config(conf_path)
else:
config = backtest_conf
# Parse ticker interval print('Using ticker_interval: {} ...'.format(args.ticker_interval))
ticker_interval = int(os.environ.get('BACKTEST_TICKER_INTERVAL') or 5)
print('Using ticker_interval: {} ...'.format(ticker_interval))
data = {} data = {}
if os.environ.get('BACKTEST_LIVE'): if args.live:
print('Downloading data for all pairs in whitelist ...') print('Downloading data for all pairs in whitelist ...')
for pair in config['exchange']['pair_whitelist']: for pair in config['exchange']['pair_whitelist']:
data[pair] = exchange.get_ticker_history(pair, ticker_interval) data[pair] = exchange.get_ticker_history(pair, args.ticker_interval)
else: else:
print('Using local backtesting data (ignoring whitelist in given config)...') print('Using local backtesting data (ignoring whitelist in given config)...')
data = load_backtesting_data(ticker_interval) data = load_backtesting_data(args.ticker_interval)
print('Using stake_currency: {} ...\nUsing stake_amount: {} ...'.format( print('Using stake_currency: {} ...\nUsing stake_amount: {} ...'.format(
config['stake_currency'], config['stake_amount'] config['stake_currency'], config['stake_amount']
@ -181,8 +163,17 @@ def test_backtest(backtest_conf, mocker):
min_date.isoformat(), max_date.isoformat() min_date.isoformat(), max_date.isoformat()
)) ))
max_open_trades = 0
if args.realistic_simulation:
print('Using max_open_trades: {} ...'.format(config['max_open_trades']))
max_open_trades = config['max_open_trades']
from freqtrade import main
main._CONF = config
# Execute backtest and print results # Execute backtest and print results
realistic = os.environ.get('BACKTEST_REALISTIC_SIMULATION') results = backtest(
results = backtest(config, preprocess(data), mocker, get_max_open_trades(config), realistic) config, preprocess(data), max_open_trades, args.realistic_simulation
)
print('====================== BACKTESTING REPORT ======================================\n\n') print('====================== BACKTESTING REPORT ======================================\n\n')
print(generate_text_table(data, results, config['stake_currency'])) print(generate_text_table(data, results, config['stake_currency']))

View File

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

View File

@ -1,5 +1,6 @@
# pragma pylint: disable=missing-docstring,W0621 # pragma pylint: disable=missing-docstring,W0621
import json import json
from unittest.mock import MagicMock
import arrow import arrow
import pytest import pytest
@ -35,20 +36,30 @@ def test_populates_sell_trend(result):
def test_returns_latest_buy_signal(mocker): def test_returns_latest_buy_signal(mocker):
buydf = DataFrame([{'buy': 1, 'date': arrow.utcnow()}]) mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock())
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf) mocker.patch(
'freqtrade.analyze.analyze_ticker',
return_value=DataFrame([{'buy': 1, 'date': arrow.utcnow()}])
)
assert get_signal('BTC-ETH', SignalType.BUY) assert get_signal('BTC-ETH', SignalType.BUY)
buydf = DataFrame([{'buy': 0, 'date': arrow.utcnow()}]) mocker.patch(
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf) 'freqtrade.analyze.analyze_ticker',
return_value=DataFrame([{'buy': 0, 'date': arrow.utcnow()}])
)
assert not get_signal('BTC-ETH', SignalType.BUY) assert not get_signal('BTC-ETH', SignalType.BUY)
def test_returns_latest_sell_signal(mocker): def test_returns_latest_sell_signal(mocker):
selldf = DataFrame([{'sell': 1, 'date': arrow.utcnow()}]) mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock())
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=selldf) mocker.patch(
'freqtrade.analyze.analyze_ticker',
return_value=DataFrame([{'sell': 1, 'date': arrow.utcnow()}])
)
assert get_signal('BTC-ETH', SignalType.SELL) assert get_signal('BTC-ETH', SignalType.SELL)
selldf = DataFrame([{'sell': 0, 'date': arrow.utcnow()}]) mocker.patch(
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=selldf) 'freqtrade.analyze.analyze_ticker',
return_value=DataFrame([{'sell': 0, 'date': arrow.utcnow()}])
)
assert not get_signal('BTC-ETH', SignalType.SELL) assert not get_signal('BTC-ETH', SignalType.SELL)

View File

@ -11,9 +11,9 @@ from pandas import DataFrame
from freqtrade import exchange from freqtrade import exchange
from freqtrade.exchange import Bittrex from freqtrade.exchange import Bittrex
from freqtrade.optimize.backtesting import backtest, format_results
from freqtrade.optimize.backtesting import preprocess
from freqtrade.tests import load_backtesting_data from freqtrade.tests import load_backtesting_data
from freqtrade.tests.test_backtesting import backtest, format_results
from freqtrade.tests.test_backtesting import preprocess
from freqtrade.vendor.qtpylib.indicators import crossed_above from freqtrade.vendor.qtpylib.indicators import crossed_above
logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot

View File

@ -1,15 +1,13 @@
# pragma pylint: disable=missing-docstring,C0103 # pragma pylint: disable=missing-docstring,C0103
import json import json
import os
import time import time
from argparse import Namespace
from copy import deepcopy from copy import deepcopy
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pytest import pytest
from jsonschema import ValidationError from jsonschema import ValidationError
from freqtrade.misc import throttle, parse_args, start_backtesting, load_config from freqtrade.misc import throttle, parse_args, load_config
def test_throttle(): def test_throttle():
@ -64,7 +62,7 @@ def test_parse_args_dynamic_whitelist():
def test_parse_args_backtesting(mocker): def test_parse_args_backtesting(mocker):
backtesting_mock = mocker.patch('freqtrade.misc.start_backtesting', MagicMock()) backtesting_mock = mocker.patch('freqtrade.optimize.backtesting.start', MagicMock())
args = parse_args(['backtesting']) args = parse_args(['backtesting'])
assert args is None assert args is None
assert backtesting_mock.call_count == 1 assert backtesting_mock.call_count == 1
@ -87,7 +85,7 @@ def test_parse_args_backtesting_invalid():
def test_parse_args_backtesting_custom(mocker): def test_parse_args_backtesting_custom(mocker):
backtesting_mock = mocker.patch('freqtrade.misc.start_backtesting', MagicMock()) backtesting_mock = mocker.patch('freqtrade.optimize.backtesting.start', MagicMock())
args = parse_args(['-c', 'test_conf.json', 'backtesting', '--live', '--ticker-interval', '1']) args = parse_args(['-c', 'test_conf.json', 'backtesting', '--live', '--ticker-interval', '1'])
assert args is None assert args is None
assert backtesting_mock.call_count == 1 assert backtesting_mock.call_count == 1
@ -101,31 +99,6 @@ def test_parse_args_backtesting_custom(mocker):
assert call_args.ticker_interval == 1 assert call_args.ticker_interval == 1
def test_start_backtesting(mocker):
pytest_mock = mocker.patch('pytest.main', MagicMock())
env_mock = mocker.patch('os.environ', {})
args = Namespace(
config='config.json',
live=True,
loglevel=20,
ticker_interval=1,
realistic_simulation=True,
)
start_backtesting(args)
assert env_mock == {
'BACKTEST': 'true',
'BACKTEST_LIVE': 'true',
'BACKTEST_CONFIG': 'config.json',
'BACKTEST_TICKER_INTERVAL': '1',
'BACKTEST_REALISTIC_SIMULATION': 'true',
}
assert pytest_mock.call_count == 1
main_call_args = pytest_mock.call_args[0][0]
assert main_call_args[0] == '-s'
assert main_call_args[1].endswith(os.path.join('freqtrade', 'tests', 'test_backtesting.py'))
def test_load_config(default_conf, mocker): def test_load_config(default_conf, mocker):
file_mock = mocker.patch('freqtrade.misc.open', mocker.mock_open( file_mock = mocker.patch('freqtrade.misc.open', mocker.mock_open(
read_data=json.dumps(default_conf) read_data=json.dumps(default_conf)

View File

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