diff --git a/freqtrade/analyze.py b/freqtrade/analyze.py index ba57d66c5..8edf86340 100644 --- a/freqtrade/analyze.py +++ b/freqtrade/analyze.py @@ -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 diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 97611601b..784e4903e 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -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', diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py new file mode 100644 index 000000000..4cffcee10 --- /dev/null +++ b/freqtrade/optimize/__init__.py @@ -0,0 +1 @@ +from . import backtesting diff --git a/freqtrade/tests/test_backtesting.py b/freqtrade/optimize/backtesting.py similarity index 81% rename from freqtrade/tests/test_backtesting.py rename to freqtrade/optimize/backtesting.py index 36c701426..ea861ee84 100644 --- a/freqtrade/tests/test_backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -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'])) diff --git a/freqtrade/tests/__init__.py b/freqtrade/tests/__init__.py index c5cc708c9..ebebe7c98 100644 --- a/freqtrade/tests/__init__.py +++ b/freqtrade/tests/__init__.py @@ -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, diff --git a/freqtrade/tests/test_analyze.py b/freqtrade/tests/test_analyze.py index c62639997..5af612a7e 100644 --- a/freqtrade/tests/test_analyze.py +++ b/freqtrade/tests/test_analyze.py @@ -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) diff --git a/freqtrade/tests/test_hyperopt.py b/freqtrade/tests/test_hyperopt.py index ab4b34674..5face5837 100644 --- a/freqtrade/tests/test_hyperopt.py +++ b/freqtrade/tests/test_hyperopt.py @@ -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 diff --git a/freqtrade/tests/test_misc.py b/freqtrade/tests/test_misc.py index a6f61d58b..9748deeea 100644 --- a/freqtrade/tests/test_misc.py +++ b/freqtrade/tests/test_misc.py @@ -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) diff --git a/freqtrade/tests/test_optimize_backtesting.py b/freqtrade/tests/test_optimize_backtesting.py new file mode 100644 index 000000000..36f4cd144 --- /dev/null +++ b/freqtrade/tests/test_optimize_backtesting.py @@ -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 +