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
from datetime import timedelta
from enum import Enum
from typing import List, Dict
import arrow
import talib.abstract as ta
@ -113,18 +114,13 @@ def populate_sell_trend(dataframe: DataFrame) -> 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
:return DataFrame with ticker data and indicator data
"""
ticker_hist = get_ticker_history(pair)
if not ticker_hist:
logger.warning('Empty ticker history for pair %s', pair)
return DataFrame()
dataframe = parse_ticker_dataframe(ticker_hist)
dataframe = parse_ticker_dataframe(ticker_history)
dataframe = populate_indicators(dataframe)
dataframe = populate_buy_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
: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:
dataframe = analyze_ticker(pair)
dataframe = analyze_ticker(ticker_hist)
except ValueError as ex:
logger.warning('Unable to analyze ticker for pair %s: %s', pair, str(ex))
return False

View File

@ -2,7 +2,6 @@ import argparse
import enum
import json
import logging
import os
import time
from typing import Any, Callable, List, Dict
@ -129,9 +128,11 @@ def parse_args(args: List[str]):
def build_subcommands(parser: argparse.ArgumentParser) -> None:
""" Builds and attaches all subcommands """
from freqtrade.optimize import backtesting
subparsers = parser.add_subparsers(dest='subparser')
backtest = subparsers.add_parser('backtesting', help='backtesting module')
backtest.set_defaults(func=start_backtesting)
backtest.set_defaults(func=backtesting.start)
backtest.add_argument(
'-l', '--live',
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
CONF_SCHEMA = {
'type': 'object',

View File

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

View File

@ -2,11 +2,9 @@
import logging
import os
from typing import Tuple, Dict
import arrow
import pytest
from pandas import DataFrame
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)
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
:param config: config to use
: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 realistic: do we try to simulate realistic trades? (default: True)
:return: DataFrame
@ -96,7 +94,6 @@ def backtest(config: Dict, processed, mocker, max_open_trades=0, realistic=True)
trades = []
trade_count_lock = {}
exchange._API = Bittrex({'key': '', 'secret': ''})
mocker.patch.dict('freqtrade.main._CONF', config)
for pair, pair_data in processed.items():
pair_data['buy'], pair_data['sell'] = 0, 0
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)
def get_max_open_trades(config):
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):
def start(args):
print('')
exchange._API = Bittrex({'key': '', 'secret': ''})
# Load configuration file based on env variable
conf_path = os.environ.get('BACKTEST_CONFIG')
if conf_path:
print('Using config: {} ...'.format(conf_path))
config = load_config(conf_path)
else:
config = backtest_conf
print('Using config: {} ...'.format(args.config))
config = load_config(args.config)
# Parse ticker interval
ticker_interval = int(os.environ.get('BACKTEST_TICKER_INTERVAL') or 5)
print('Using ticker_interval: {} ...'.format(ticker_interval))
print('Using ticker_interval: {} ...'.format(args.ticker_interval))
data = {}
if os.environ.get('BACKTEST_LIVE'):
if args.live:
print('Downloading data for all pairs in 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:
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(
config['stake_currency'], config['stake_amount']
@ -181,8 +163,17 @@ def test_backtest(backtest_conf, mocker):
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
realistic = os.environ.get('BACKTEST_REALISTIC_SIMULATION')
results = backtest(config, preprocess(data), mocker, get_max_open_trades(config), realistic)
results = backtest(
config, preprocess(data), max_open_trades, args.realistic_simulation
)
print('====================== BACKTESTING REPORT ======================================\n\n')
print(generate_text_table(data, results, config['stake_currency']))

View File

@ -1,16 +1,17 @@
# pragma pylint: disable=missing-docstring
import json
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__))
result = {}
pairs = [
_pairs = pairs or [
'BTC_BCC', 'BTC_ETH', 'BTC_DASH', 'BTC_POWR', 'BTC_ETC',
'BTC_VTC', 'BTC_WAVES', 'BTC_LSK', 'BTC_XLM', 'BTC_OK',
]
for pair in pairs:
for pair in _pairs:
with open('{abspath}/testdata/{pair}-{ticker_interval}.json'.format(
abspath=path,
pair=pair,

View File

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

View File

@ -11,9 +11,9 @@ from pandas import DataFrame
from freqtrade import exchange
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.test_backtesting import backtest, format_results
from freqtrade.tests.test_backtesting import preprocess
from freqtrade.vendor.qtpylib.indicators import crossed_above
logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot

View File

@ -1,15 +1,13 @@
# pragma pylint: disable=missing-docstring,C0103
import json
import os
import time
from argparse import Namespace
from copy import deepcopy
from unittest.mock import MagicMock
import pytest
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():
@ -64,7 +62,7 @@ def test_parse_args_dynamic_whitelist():
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'])
assert args is None
assert backtesting_mock.call_count == 1
@ -87,7 +85,7 @@ def test_parse_args_backtesting_invalid():
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'])
assert args is None
assert backtesting_mock.call_count == 1
@ -101,31 +99,6 @@ def test_parse_args_backtesting_custom(mocker):
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):
file_mock = mocker.patch('freqtrade.misc.open', mocker.mock_open(
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