move backtesting to freqtrade.optimize.backtesting
This commit is contained in:
parent
858d2329e5
commit
3b37f77a4d
@ -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
|
||||
|
@ -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',
|
||||
|
1
freqtrade/optimize/__init__.py
Normal file
1
freqtrade/optimize/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import backtesting
|
@ -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']))
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
18
freqtrade/tests/test_optimize_backtesting.py
Normal file
18
freqtrade/tests/test_optimize_backtesting.py
Normal 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
|
||||
|
Loading…
Reference in New Issue
Block a user