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
args = build_arg_parser().parse_args()
# Check if subcommand has been selected
if hasattr(args, 'func'):
args.func(args)
exit(0)
# Initialize logger
logging.basicConfig(
level=args.loglevel,

View File

@ -1,6 +1,7 @@
import argparse
import enum
import logging
import os
import time
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',
action='store_true',
)
build_subcommands(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
CONF_SCHEMA = {
'type': 'object',

View File

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

View File

@ -1,10 +1,13 @@
# pragma pylint: disable=missing-docstring
from typing import Dict
import json
import logging
import os
from typing import Tuple, Dict
import pytest
import arrow
import pytest
from arrow import Arrow
from pandas import DataFrame
from freqtrade import exchange
@ -14,15 +17,20 @@ from freqtrade.exchange import Bittrex
from freqtrade.main import min_roi_reached
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):
return 'Made {} buys. Average profit {:.2f}%. 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 format_results(results: DataFrame):
return 'Made {} buys. Average profit {:.2f}%. ' \
'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(format_results(results[results.currency == pair]))
@ -34,11 +42,21 @@ def preprocess(backdata) -> Dict[str, DataFrame]:
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):
trades = []
exchange._API = Bittrex({'key': '', 'secret': ''})
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():
pair_data['buy'] = 0
pair_data['sell'] = 0
@ -62,12 +80,35 @@ def backtest(backtest_conf, processed, mocker):
return DataFrame.from_records(trades, columns=labels)
@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set")
def test_backtest(backtest_conf, backdata, mocker, report=True):
results = backtest(backtest_conf, preprocess(backdata), mocker)
@pytest.mark.skipif(not os.environ.get('BACKTEST'), reason="BACKTEST not set")
def test_backtest(backtest_conf, 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 ================================')
for pair in backdata:
for pair in data:
print_pair_results(pair, results)
print('TOTAL OVER ALL TRADES:')
print(format_results(results))