implement backtest subcommand

This commit is contained in:
gcarq 2017-11-14 22:15:24 +01:00
parent 77887d6fbc
commit bb4a9ed20f
4 changed files with 94 additions and 14 deletions

View File

@ -318,6 +318,11 @@ def main():
global _CONF global _CONF
args = build_arg_parser().parse_args() args = build_arg_parser().parse_args()
# Check if subcommand has been selected
if hasattr(args, 'func'):
args.func(args)
exit(0)
# Initialize logger # Initialize logger
logging.basicConfig( logging.basicConfig(
level=args.loglevel, level=args.loglevel,

View File

@ -1,6 +1,7 @@
import argparse import argparse
import enum import enum
import logging import logging
import os
import time import time
from typing import Any, Callable from typing import Any, Callable
@ -92,9 +93,40 @@ def build_arg_parser() -> argparse.ArgumentParser:
help='dynamically generate and update whitelist based on 24h BaseVolume', help='dynamically generate and update whitelist based on 24h BaseVolume',
action='store_true', action='store_true',
) )
build_subcommands(parser)
return parser return parser
def build_subcommands(parser: argparse.ArgumentParser) -> None:
""" Builds and attaches all subcommands """
subparsers = parser.add_subparsers(dest='subparser')
backtest = subparsers.add_parser('backtest', help='backtesting module')
backtest.set_defaults(func=start_backtesting)
backtest.add_argument(
'-l', '--live',
action='store_true',
dest='live',
help='using live data',
)
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,
})
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

@ -3,6 +3,7 @@ import json
from datetime import datetime from datetime import datetime
from unittest.mock import MagicMock from unittest.mock import MagicMock
import os
import pytest import pytest
from jsonschema import validate from jsonschema import validate
from telegram import Message, Chat, Update from telegram import Message, Chat, Update
@ -67,11 +68,12 @@ def backtest_conf():
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def backdata(): def backdata():
path = os.path.abspath(os.path.dirname(__file__))
result = {} result = {}
for pair in ['btc-neo', 'btc-eth', 'btc-omg', 'btc-edg', 'btc-pay', for pair in ['btc-neo', 'btc-eth', 'btc-omg', 'btc-edg', 'btc-pay',
'btc-pivx', 'btc-qtum', 'btc-mtl', 'btc-etc', 'btc-ltc']: 'btc-pivx', 'btc-qtum', 'btc-mtl', 'btc-etc', 'btc-ltc']:
with open('freqtrade/tests/testdata/' + pair + '.json') as data_file: with open('{abspath}/testdata/{pair}.json'.format(abspath=path, pair=pair)) as fp:
result[pair] = json.load(data_file) result[pair] = json.load(fp)
return result return result

View File

@ -1,10 +1,13 @@
# pragma pylint: disable=missing-docstring # pragma pylint: disable=missing-docstring
from typing import Dict
import json
import logging import logging
import os import os
from typing import Tuple, Dict
import pytest
import arrow import arrow
import pytest
from arrow import Arrow
from pandas import DataFrame from pandas import DataFrame
from freqtrade import exchange from freqtrade import exchange
@ -14,15 +17,20 @@ from freqtrade.exchange import Bittrex
from freqtrade.main import min_roi_reached from freqtrade.main import min_roi_reached
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot logger = logging.getLogger(__name__)
def format_results(results): def format_results(results: DataFrame):
return 'Made {} buys. Average profit {:.2f}%. Total profit was {:.3f}. Average duration {:.1f} mins.'.format( return 'Made {} buys. Average profit {:.2f}%. ' \
len(results.index), results.profit.mean() * 100.0, results.profit.sum(), results.duration.mean() * 5) 'Total profit was {:.3f}. Average duration {:.1f} mins.'.format(
len(results.index),
results.profit.mean() * 100.0,
results.profit.sum(),
results.duration.mean() * 5,
)
def print_pair_results(pair, results): def print_pair_results(pair: str, results: DataFrame):
print('For currency {}:'.format(pair)) print('For currency {}:'.format(pair))
print(format_results(results[results.currency == pair])) print(format_results(results[results.currency == pair]))
@ -34,11 +42,21 @@ def preprocess(backdata) -> Dict[str, DataFrame]:
return processed return processed
def get_timeframe(backdata: Dict[str, Dict]) -> Tuple[Arrow, Arrow]:
min_date, max_date = None, None
for values in backdata.values():
values = sorted(values, key=lambda d: arrow.get(d['T']))
if not min_date or values[0]['T'] < min_date:
min_date = values[0]['T']
if not max_date or values[-1]['T'] > max_date:
max_date = values[-1]['T']
return arrow.get(min_date), arrow.get(max_date)
def backtest(backtest_conf, processed, mocker): def backtest(backtest_conf, processed, mocker):
trades = [] trades = []
exchange._API = Bittrex({'key': '', 'secret': ''}) exchange._API = Bittrex({'key': '', 'secret': ''})
mocker.patch.dict('freqtrade.main._CONF', backtest_conf) mocker.patch.dict('freqtrade.main._CONF', backtest_conf)
mocker.patch('arrow.utcnow', return_value=arrow.get('2017-08-20T14:50:00'))
for pair, pair_data in processed.items(): for pair, pair_data in processed.items():
pair_data['buy'] = 0 pair_data['buy'] = 0
pair_data['sell'] = 0 pair_data['sell'] = 0
@ -62,12 +80,35 @@ def backtest(backtest_conf, processed, mocker):
return DataFrame.from_records(trades, columns=labels) return DataFrame.from_records(trades, columns=labels)
@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set") @pytest.mark.skipif(not os.environ.get('BACKTEST'), reason="BACKTEST not set")
def test_backtest(backtest_conf, backdata, mocker, report=True): def test_backtest(backtest_conf, backdata, mocker):
results = backtest(backtest_conf, preprocess(backdata), mocker) print('')
config = None
conf_path = os.environ.get('BACKTEST_CONFIG')
if conf_path:
print('Using config: {} ...'.format(conf_path))
with open(conf_path, 'r') as fp:
config = json.load(fp)
livedata = {}
if os.environ.get('BACKTEST_LIVE'):
print('Downloading data for all pairs in whitelist ...'.format(conf_path))
exchange._API = Bittrex({'key': '', 'secret': ''})
for pair in config['exchange']['pair_whitelist']:
livedata[pair] = exchange.get_ticker_history(pair)
config = config or backtest_conf
data = livedata or backdata
min_date, max_date = get_timeframe(data)
print('Measuring data from {} up to {} ...'.format(
min_date.isoformat(), max_date.isoformat()
))
results = backtest(config, preprocess(data), mocker)
print('====================== BACKTESTING REPORT ================================') print('====================== BACKTESTING REPORT ================================')
for pair in backdata: for pair in data:
print_pair_results(pair, results) print_pair_results(pair, results)
print('TOTAL OVER ALL TRADES:') print('TOTAL OVER ALL TRADES:')
print(format_results(results)) print(format_results(results))