diff --git a/.gitignore b/.gitignore index 2d4af4dac..9b0606756 100644 --- a/.gitignore +++ b/.gitignore @@ -76,6 +76,8 @@ target/ config.json preprocessor.py *.sqlite +.hyperopt +logfile.txt .env .venv diff --git a/.travis.yml b/.travis.yml index 1d6a6a9b6..461324f45 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,9 +4,6 @@ os: language: python python: - 3.6 -env: - - BACKTEST= - - BACKTEST=true addons: apt: packages: diff --git a/README.md b/README.md index 398056079..28cba0855 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ See the example below: "40": 0.0, # Sell after 40 minutes if the profit is not negative "30": 0.01, # Sell after 30 minutes if there is at least 1% profit "20": 0.02, # Sell after 20 minutes if there is at least 2% profit - "0": 0.04 # Sell immediately if there is at least 4% profit + "0": 0.04 # Sell immediately if there is at least 4% profit }, ``` @@ -164,25 +164,39 @@ optional arguments: Backtesting also uses the config specified via `-c/--config`. ``` -usage: freqtrade backtesting [-h] [-l] [-i INT] +usage: freqtrade backtesting [-h] [-l] [-i INT] [--realistic-simulation] optional arguments: -h, --help show this help message and exit -l, --live using live data -i INT, --ticker-interval INT specify ticker interval in minutes (default: 5) + --realistic-simulation + uses max_open_trades from config to simulate real + world limitations + ``` +### Hyperopt + +It is possible to use hyperopt for trading strategy optimization. +Hyperopt uses an internal config named `OPTIMIZE_CONFIG` located in `freqtrade/optimize/hyperopt.py`. + +``` +usage: freqtrade hyperopt [-h] [-e INT] [--use-mongodb] + +optional arguments: + -h, --help show this help message and exit + -e INT, --epochs INT specify number of epochs (default: 100) + --use-mongodb parallelize evaluations with mongodb (requires mongod + in PATH) + +``` ### Execute tests ``` -$ pytest -``` -This will by default skip the slow running backtest set. To run backtest set: - -``` -$ BACKTEST=true pytest -s freqtrade/tests/test_backtesting.py +$ pytest freqtrade ``` ### Contributing diff --git a/freqtrade/analyze.py b/freqtrade/analyze.py index ba57d66c5..d586077db 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 @@ -14,6 +15,7 @@ from freqtrade.vendor.qtpylib.indicators import awesome_oscillator, crossed_abov logger = logging.getLogger(__name__) + class SignalType(Enum): """ Enum to distinguish between buy and sell signals """ BUY = "buy" @@ -113,18 +115,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 +134,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..dcae19ac4 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,16 +128,20 @@ def parse_args(args: List[str]): def build_subcommands(parser: argparse.ArgumentParser) -> None: """ Builds and attaches all subcommands """ + from freqtrade.optimize import backtesting, hyperopt + subparsers = parser.add_subparsers(dest='subparser') - backtest = subparsers.add_parser('backtesting', help='backtesting module') - backtest.set_defaults(func=start_backtesting) - backtest.add_argument( + + # Add backtesting subcommand + backtesting_cmd = subparsers.add_parser('backtesting', help='backtesting module') + backtesting_cmd.set_defaults(func=backtesting.start) + backtesting_cmd.add_argument( '-l', '--live', action='store_true', dest='live', help='using live data', ) - backtest.add_argument( + backtesting_cmd.add_argument( '-i', '--ticker-interval', help='specify ticker interval in minutes (default: 5)', dest='ticker_interval', @@ -146,31 +149,30 @@ def build_subcommands(parser: argparse.ArgumentParser) -> None: type=int, metavar='INT', ) - backtest.add_argument( + backtesting_cmd.add_argument( '--realistic-simulation', help='uses max_open_trades from config to simulate real world limitations', action='store_true', dest='realistic_simulation', ) - -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]) + # Add hyperopt subcommand + hyperopt_cmd = subparsers.add_parser('hyperopt', help='hyperopt module') + hyperopt_cmd.set_defaults(func=hyperopt.start) + hyperopt_cmd.add_argument( + '-e', '--epochs', + help='specify number of epochs (default: 100)', + dest='epochs', + default=100, + type=int, + metavar='INT', + ) + hyperopt_cmd.add_argument( + '--use-mongodb', + help='parallelize evaluations with mongodb (requires mongod in PATH)', + dest='mongodb', + action='store_true', + ) # Required json-schema for user specified config diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py new file mode 100644 index 000000000..1bd1a97e8 --- /dev/null +++ b/freqtrade/optimize/__init__.py @@ -0,0 +1,41 @@ +# pragma pylint: disable=missing-docstring + + +import json +import os +from typing import Optional, List, Dict + +from pandas import DataFrame + +from freqtrade.analyze import populate_indicators, parse_ticker_dataframe + + +def load_data(ticker_interval: int = 5, pairs: Optional[List[str]] = None) -> Dict[str, List]: + """ + Loads ticker history data for the given parameters + :param ticker_interval: ticker interval in minutes + :param pairs: list of pairs + :return: dict + """ + path = os.path.abspath(os.path.dirname(__file__)) + result = {} + _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: + with open('{abspath}/../tests/testdata/{pair}-{ticker_interval}.json'.format( + abspath=path, + pair=pair, + ticker_interval=ticker_interval, + )) as tickerdata: + result[pair] = json.load(tickerdata) + return result + + +def preprocess(tickerdata: Dict[str, List]) -> Dict[str, DataFrame]: + """Creates a dataframe and populates indicators for given ticker data""" + processed = {} + for pair, pair_data in tickerdata.items(): + processed[pair] = populate_indicators(parse_ticker_dataframe(pair_data)) + return processed diff --git a/freqtrade/tests/test_backtesting.py b/freqtrade/optimize/backtesting.py similarity index 63% rename from freqtrade/tests/test_backtesting.py rename to freqtrade/optimize/backtesting.py index 36c701426..49f7e79a8 100644 --- a/freqtrade/tests/test_backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -2,43 +2,23 @@ import logging -import os from typing import Tuple, Dict import arrow -import pytest from pandas import DataFrame from tabulate import tabulate from freqtrade import exchange -from freqtrade.analyze import parse_ticker_dataframe, populate_indicators, \ - populate_buy_trend, populate_sell_trend +from freqtrade.analyze import populate_buy_trend, populate_sell_trend from freqtrade.exchange import Bittrex from freqtrade.main import min_roi_reached from freqtrade.misc import load_config +from freqtrade.optimize import load_data, preprocess from freqtrade.persistence import Trade -from freqtrade.tests import load_backtesting_data logger = logging.getLogger(__name__) -def format_results(results: DataFrame): - return ('Made {:6d} buys. Average profit {: 5.2f}%. ' - 'Total profit was {: 7.3f}. Average duration {:5.1f} mins.').format( - len(results.index), - results.profit.mean() * 100.0, - results.profit.sum(), - results.duration.mean() * 5, - ) - - -def preprocess(backdata) -> Dict[str, DataFrame]: - processed = {} - for pair, pair_data in backdata.items(): - processed[pair] = populate_indicators(parse_ticker_dataframe(pair_data)) - return processed - - def get_timeframe(data: Dict[str, Dict]) -> Tuple[arrow.Arrow, arrow.Arrow]: """ Get the maximum timeframe for the given backtest data @@ -83,12 +63,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 +76,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,51 +117,50 @@ 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'] +def start(args): + # Initialize logger + logging.basicConfig( + level=args.loglevel, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + ) - -@pytest.mark.skipif(not os.environ.get('BACKTEST'), reason="BACKTEST not set") -def test_backtest(backtest_conf, mocker): - 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 + logger.info('Using config: %s ...', 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)) + logger.info('Using ticker_interval: %s ...', args.ticker_interval) data = {} - if os.environ.get('BACKTEST_LIVE'): - print('Downloading data for all pairs in whitelist ...') + if args.live: + logger.info('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) + logger.info('Using local backtesting data (ignoring whitelist in given config) ...') + data = load_data(args.ticker_interval) - print('Using stake_currency: {} ...\nUsing stake_amount: {} ...'.format( - config['stake_currency'], config['stake_amount'] - )) + logger.info('Using stake_currency: %s ...', config['stake_currency']) + logger.info('Using stake_amount: %s ...', config['stake_amount']) # Print timeframe min_date, max_date = get_timeframe(data) - print('Measuring data from {} up to {} ...'.format( - min_date.isoformat(), max_date.isoformat() - )) + logger.info('Measuring data from %s up to %s ...', min_date.isoformat(), max_date.isoformat()) + + max_open_trades = 0 + if args.realistic_simulation: + logger.info('Using max_open_trades: %s ...', config['max_open_trades']) + max_open_trades = config['max_open_trades'] + + # Monkey patch config + 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) - print('====================== BACKTESTING REPORT ======================================\n\n') - print(generate_text_table(data, results, config['stake_currency'])) + results = backtest( + config, preprocess(data), max_open_trades, args.realistic_simulation + ) + logger.info( + '\n====================== BACKTESTING REPORT ======================================\n%s', + generate_text_table(data, results, config['stake_currency']) + ) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py new file mode 100644 index 000000000..36eb0d275 --- /dev/null +++ b/freqtrade/optimize/hyperopt.py @@ -0,0 +1,205 @@ +# pragma pylint: disable=missing-docstring,W0212 + + +import json +import logging +from functools import reduce +from math import exp +from operator import itemgetter + +from hyperopt import fmin, tpe, hp, Trials, STATUS_OK +from hyperopt.mongoexp import MongoTrials +from pandas import DataFrame + +from freqtrade import exchange, optimize +from freqtrade.exchange import Bittrex +from freqtrade.optimize.backtesting import backtest +from freqtrade.vendor.qtpylib.indicators import crossed_above + +# Remove noisy log messages +logging.getLogger('hyperopt.mongoexp').setLevel(logging.WARNING) + +logger = logging.getLogger(__name__) + + +# set TARGET_TRADES to suit your number concurrent trades so its realistic to 20days of data +TARGET_TRADES = 1100 +TOTAL_TRIES = None +_CURRENT_TRIES = 0 + +# Configuration and data used by hyperopt +PROCESSED = optimize.preprocess(optimize.load_data()) +OPTIMIZE_CONFIG = { + 'max_open_trades': 3, + 'stake_currency': 'BTC', + 'stake_amount': 0.01, + 'minimal_roi': { + '40': 0.0, + '30': 0.01, + '20': 0.02, + '0': 0.04, + }, + 'stoploss': -0.10, +} + +# Monkey patch config +from freqtrade import main +main._CONF = OPTIMIZE_CONFIG + + +SPACE = { + 'mfi': hp.choice('mfi', [ + {'enabled': False}, + {'enabled': True, 'value': hp.quniform('mfi-value', 5, 25, 1)} + ]), + 'fastd': hp.choice('fastd', [ + {'enabled': False}, + {'enabled': True, 'value': hp.quniform('fastd-value', 10, 50, 1)} + ]), + 'adx': hp.choice('adx', [ + {'enabled': False}, + {'enabled': True, 'value': hp.quniform('adx-value', 15, 50, 1)} + ]), + 'rsi': hp.choice('rsi', [ + {'enabled': False}, + {'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 1)} + ]), + 'uptrend_long_ema': hp.choice('uptrend_long_ema', [ + {'enabled': False}, + {'enabled': True} + ]), + 'uptrend_short_ema': hp.choice('uptrend_short_ema', [ + {'enabled': False}, + {'enabled': True} + ]), + 'over_sar': hp.choice('over_sar', [ + {'enabled': False}, + {'enabled': True} + ]), + 'green_candle': hp.choice('green_candle', [ + {'enabled': False}, + {'enabled': True} + ]), + 'uptrend_sma': hp.choice('uptrend_sma', [ + {'enabled': False}, + {'enabled': True} + ]), + 'trigger': hp.choice('trigger', [ + {'type': 'lower_bb'}, + {'type': 'faststoch10'}, + {'type': 'ao_cross_zero'}, + {'type': 'ema5_cross_ema10'}, + {'type': 'macd_cross_signal'}, + {'type': 'sar_reversal'}, + {'type': 'stochf_cross'}, + {'type': 'ht_sine'}, + ]), +} + + +def optimizer(params): + global _CURRENT_TRIES + + from freqtrade.optimize import backtesting + backtesting.populate_buy_trend = buy_strategy_generator(params) + + results = backtest(OPTIMIZE_CONFIG, PROCESSED) + + result = format_results(results) + + total_profit = results.profit.sum() * 1000 + trade_count = len(results.index) + + trade_loss = 1 - 0.35 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.2) + profit_loss = max(0, 1 - total_profit / 10000) # max profit 10000 + + _CURRENT_TRIES += 1 + logger.info('{:5d}/{}: {}'.format(_CURRENT_TRIES, TOTAL_TRIES, result)) + + return { + 'loss': trade_loss + profit_loss, + 'status': STATUS_OK, + 'result': result + } + + +def format_results(results: DataFrame): + return ('Made {:6d} buys. Average profit {: 5.2f}%. ' + 'Total profit was {: 7.3f}. Average duration {:5.1f} mins.').format( + len(results.index), + results.profit.mean() * 100.0, + results.profit.sum(), + results.duration.mean() * 5, + ) + + +def buy_strategy_generator(params): + def populate_buy_trend(dataframe: DataFrame) -> DataFrame: + conditions = [] + # GUARDS AND TRENDS + if params['uptrend_long_ema']['enabled']: + conditions.append(dataframe['ema50'] > dataframe['ema100']) + if params['uptrend_short_ema']['enabled']: + conditions.append(dataframe['ema5'] > dataframe['ema10']) + if params['mfi']['enabled']: + conditions.append(dataframe['mfi'] < params['mfi']['value']) + if params['fastd']['enabled']: + conditions.append(dataframe['fastd'] < params['fastd']['value']) + if params['adx']['enabled']: + conditions.append(dataframe['adx'] > params['adx']['value']) + if params['rsi']['enabled']: + conditions.append(dataframe['rsi'] < params['rsi']['value']) + if params['over_sar']['enabled']: + conditions.append(dataframe['close'] > dataframe['sar']) + if params['green_candle']['enabled']: + conditions.append(dataframe['close'] > dataframe['open']) + if params['uptrend_sma']['enabled']: + prevsma = dataframe['sma'].shift(1) + conditions.append(dataframe['sma'] > prevsma) + + # TRIGGERS + triggers = { + 'lower_bb': dataframe['tema'] <= dataframe['blower'], + 'faststoch10': (crossed_above(dataframe['fastd'], 10.0)), + 'ao_cross_zero': (crossed_above(dataframe['ao'], 0.0)), + 'ema5_cross_ema10': (crossed_above(dataframe['ema5'], dataframe['ema10'])), + 'macd_cross_signal': (crossed_above(dataframe['macd'], dataframe['macdsignal'])), + 'sar_reversal': (crossed_above(dataframe['close'], dataframe['sar'])), + 'stochf_cross': (crossed_above(dataframe['fastk'], dataframe['fastd'])), + 'ht_sine': (crossed_above(dataframe['htleadsine'], dataframe['htsine'])), + } + conditions.append(triggers.get(params['trigger']['type'])) + + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'buy'] = 1 + + return dataframe + return populate_buy_trend + + +def start(args): + global TOTAL_TRIES + TOTAL_TRIES = args.epochs + + exchange._API = Bittrex({'key': '', 'secret': ''}) + + # Initialize logger + logging.basicConfig( + level=args.loglevel, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + ) + + if args.mongodb: + logger.info('Using mongodb ...') + logger.info('Start scripts/start-mongodb.sh and start-hyperopt-worker.sh manually!') + + db_name = 'freqtrade_hyperopt' + trials = MongoTrials('mongo://127.0.0.1:1234/{}/jobs'.format(db_name), exp_key='exp1') + else: + trials = Trials() + + best = fmin(fn=optimizer, space=SPACE, algo=tpe.suggest, max_evals=TOTAL_TRIES, trials=trials) + logger.info('Best parameters:\n%s', json.dumps(best, indent=4)) + results = sorted(trials.results, key=itemgetter('loss')) + logger.info('Best Result:\n%s', results[0]['result']) diff --git a/freqtrade/tests/__init__.py b/freqtrade/tests/__init__.py index c5cc708c9..e69de29bb 100644 --- a/freqtrade/tests/__init__.py +++ b/freqtrade/tests/__init__.py @@ -1,20 +0,0 @@ -# pragma pylint: disable=missing-docstring -import json -import os - - -def load_backtesting_data(ticker_interval: int = 5): - path = os.path.abspath(os.path.dirname(__file__)) - result = {} - pairs = [ - 'BTC_BCC', 'BTC_ETH', 'BTC_DASH', 'BTC_POWR', 'BTC_ETC', - 'BTC_VTC', 'BTC_WAVES', 'BTC_LSK', 'BTC_XLM', 'BTC_OK', - ] - for pair in pairs: - with open('{abspath}/testdata/{pair}-{ticker_interval}.json'.format( - abspath=path, - pair=pair, - ticker_interval=ticker_interval, - )) as tickerdata: - result[pair] = json.load(tickerdata) - return result diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index e624e96c7..f2a9362ec 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -51,22 +51,6 @@ def default_conf(): return configuration -@pytest.fixture(scope="module") -def backtest_conf(): - return { - "max_open_trades": 3, - "stake_currency": "BTC", - "stake_amount": 0.01, - "minimal_roi": { - "40": 0.0, - "30": 0.01, - "20": 0.02, - "0": 0.04 - }, - "stoploss": -0.10 - } - - @pytest.fixture def update(): _update = Update(0) 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 deleted file mode 100644 index ab4b34674..000000000 --- a/freqtrade/tests/test_hyperopt.py +++ /dev/null @@ -1,163 +0,0 @@ -# pragma pylint: disable=missing-docstring,W0212 -import logging -import os -from functools import reduce -from math import exp -from operator import itemgetter - -import pytest -from hyperopt import fmin, tpe, hp, Trials, STATUS_OK -from pandas import DataFrame - -from freqtrade import exchange -from freqtrade.exchange import Bittrex -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 - -# set TARGET_TRADES to suit your number concurrent trades so its realistic to 20days of data -TARGET_TRADES = 1100 -TOTAL_TRIES = 4 -# pylint: disable=C0103 -current_tries = 0 - - -def buy_strategy_generator(params): - def populate_buy_trend(dataframe: DataFrame) -> DataFrame: - conditions = [] - # GUARDS AND TRENDS - if params['uptrend_long_ema']['enabled']: - conditions.append(dataframe['ema50'] > dataframe['ema100']) - if params['uptrend_short_ema']['enabled']: - conditions.append(dataframe['ema5'] > dataframe['ema10']) - if params['mfi']['enabled']: - conditions.append(dataframe['mfi'] < params['mfi']['value']) - if params['fastd']['enabled']: - conditions.append(dataframe['fastd'] < params['fastd']['value']) - if params['adx']['enabled']: - conditions.append(dataframe['adx'] > params['adx']['value']) - if params['rsi']['enabled']: - conditions.append(dataframe['rsi'] < params['rsi']['value']) - if params['over_sar']['enabled']: - conditions.append(dataframe['close'] > dataframe['sar']) - if params['green_candle']['enabled']: - conditions.append(dataframe['close'] > dataframe['open']) - if params['uptrend_sma']['enabled']: - prevsma = dataframe['sma'].shift(1) - conditions.append(dataframe['sma'] > prevsma) - - # TRIGGERS - triggers = { - 'lower_bb': dataframe['tema'] <= dataframe['blower'], - 'faststoch10': (crossed_above(dataframe['fastd'], 10.0)), - 'ao_cross_zero': (crossed_above(dataframe['ao'], 0.0)), - 'ema5_cross_ema10': (crossed_above(dataframe['ema5'], dataframe['ema10'])), - 'macd_cross_signal': (crossed_above(dataframe['macd'], dataframe['macdsignal'])), - 'sar_reversal': (crossed_above(dataframe['close'], dataframe['sar'])), - 'stochf_cross': (crossed_above(dataframe['fastk'], dataframe['fastd'])), - 'ht_sine': (crossed_above(dataframe['htleadsine'], dataframe['htsine'])), - } - conditions.append(triggers.get(params['trigger']['type'])) - - dataframe.loc[ - reduce(lambda x, y: x & y, conditions), - 'buy'] = 1 - - return dataframe - return populate_buy_trend - - -@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set") -def test_hyperopt(backtest_conf, mocker): - mocked_buy_trend = mocker.patch('freqtrade.tests.test_backtesting.populate_buy_trend') - - backdata = load_backtesting_data() - processed = preprocess(backdata) - exchange._API = Bittrex({'key': '', 'secret': ''}) - - def optimizer(params): - mocked_buy_trend.side_effect = buy_strategy_generator(params) - - results = backtest(backtest_conf, processed, mocker) - - result = format_results(results) - - total_profit = results.profit.sum() * 1000 - trade_count = len(results.index) - - trade_loss = 1 - 0.35 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.2) - profit_loss = max(0, 1 - total_profit / 10000) # max profit 10000 - - # pylint: disable=W0603 - global current_tries - current_tries += 1 - print('{:5d}/{}: {}'.format(current_tries, TOTAL_TRIES, result)) - - return { - 'loss': trade_loss + profit_loss, - 'status': STATUS_OK, - 'result': result - } - - space = { - 'mfi': hp.choice('mfi', [ - {'enabled': False}, - {'enabled': True, 'value': hp.quniform('mfi-value', 5, 25, 1)} - ]), - 'fastd': hp.choice('fastd', [ - {'enabled': False}, - {'enabled': True, 'value': hp.quniform('fastd-value', 10, 50, 1)} - ]), - 'adx': hp.choice('adx', [ - {'enabled': False}, - {'enabled': True, 'value': hp.quniform('adx-value', 15, 50, 1)} - ]), - 'rsi': hp.choice('rsi', [ - {'enabled': False}, - {'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 1)} - ]), - 'uptrend_long_ema': hp.choice('uptrend_long_ema', [ - {'enabled': False}, - {'enabled': True} - ]), - 'uptrend_short_ema': hp.choice('uptrend_short_ema', [ - {'enabled': False}, - {'enabled': True} - ]), - 'over_sar': hp.choice('over_sar', [ - {'enabled': False}, - {'enabled': True} - ]), - 'green_candle': hp.choice('green_candle', [ - {'enabled': False}, - {'enabled': True} - ]), - 'uptrend_sma': hp.choice('uptrend_sma', [ - {'enabled': False}, - {'enabled': True} - ]), - 'trigger': hp.choice('trigger', [ - {'type': 'lower_bb'}, - {'type': 'faststoch10'}, - {'type': 'ao_cross_zero'}, - {'type': 'ema5_cross_ema10'}, - {'type': 'macd_cross_signal'}, - {'type': 'sar_reversal'}, - {'type': 'stochf_cross'}, - {'type': 'ht_sine'}, - ]), - } - trials = Trials() - best = fmin(fn=optimizer, space=space, algo=tpe.suggest, max_evals=TOTAL_TRIES, trials=trials) - print('\n\n\n\n==================== HYPEROPT BACKTESTING REPORT ==============================') - print('Best parameters {}'.format(best)) - newlist = sorted(trials.results, key=itemgetter('loss')) - print('Result: {}'.format(newlist[0]['result'])) - - -if __name__ == '__main__': - # for profiling with cProfile and line_profiler - pytest.main([__file__, '-s']) diff --git a/freqtrade/tests/test_misc.py b/freqtrade/tests/test_misc.py index a6f61d58b..6f52b44b8 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 @@ -80,14 +78,14 @@ def test_parse_args_backtesting(mocker): def test_parse_args_backtesting_invalid(): with pytest.raises(SystemExit, match=r'2'): - parse_args(['--ticker-interval']) + parse_args(['backtesting --ticker-interval']) with pytest.raises(SystemExit, match=r'2'): - parse_args(['--ticker-interval', 'abc']) + parse_args(['backtesting --ticker-interval', 'abc']) 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,29 +99,31 @@ 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 +def test_parse_args_hyperopt(mocker): + hyperopt_mock = mocker.patch('freqtrade.optimize.hyperopt.start', MagicMock()) + args = parse_args(['hyperopt']) + assert args is None + assert hyperopt_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')) + call_args = hyperopt_mock.call_args[0][0] + assert call_args.config == 'config.json' + assert call_args.loglevel == 20 + assert call_args.subparser == 'hyperopt' + assert call_args.func is not None + + +def test_parse_args_hyperopt_custom(mocker): + hyperopt_mock = mocker.patch('freqtrade.optimize.hyperopt.start', MagicMock()) + args = parse_args(['-c', 'test_conf.json', 'hyperopt', '--epochs', '20']) + assert args is None + assert hyperopt_mock.call_count == 1 + + call_args = hyperopt_mock.call_args[0][0] + assert call_args.config == 'test_conf.json' + assert call_args.epochs == 20 + assert call_args.loglevel == 20 + assert call_args.subparser == 'hyperopt' + assert call_args.func is not None def test_load_config(default_conf, mocker): diff --git a/freqtrade/tests/test_optimize_backtesting.py b/freqtrade/tests/test_optimize_backtesting.py new file mode 100644 index 000000000..7986f6d35 --- /dev/null +++ b/freqtrade/tests/test_optimize_backtesting.py @@ -0,0 +1,16 @@ +# pragma pylint: disable=missing-docstring,W0212 + + +from freqtrade import exchange, optimize +from freqtrade.exchange import Bittrex +from freqtrade.optimize.backtesting import backtest + + +def test_backtest(default_conf, mocker): + mocker.patch.dict('freqtrade.main._CONF', default_conf) + exchange._API = Bittrex({'key': '', 'secret': ''}) + + data = optimize.load_data(ticker_interval=5, pairs=['BTC_ETH']) + results = backtest(default_conf, optimize.preprocess(data), 10, True) + num_resutls = len(results) + assert num_resutls > 0 diff --git a/freqtrade/tests/test_optimize_hyperopt.py b/freqtrade/tests/test_optimize_hyperopt.py new file mode 100644 index 000000000..a8bfe7dd4 --- /dev/null +++ b/freqtrade/tests/test_optimize_hyperopt.py @@ -0,0 +1,6 @@ +# pragma pylint: disable=missing-docstring,W0212 + + +def test_optimizer(default_conf, mocker): + # TODO: implement test + pass diff --git a/scripts/start-hyperopt-worker.py b/scripts/start-hyperopt-worker.py new file mode 100755 index 000000000..8b0ae6326 --- /dev/null +++ b/scripts/start-hyperopt-worker.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +import multiprocessing +import os +import subprocess + +PROC_COUNT = multiprocessing.cpu_count() - 1 +DB_NAME = 'freqtrade_hyperopt' +WORK_DIR = os.path.join( + os.path.sep, + os.path.abspath(os.path.dirname(__file__)), + '..', '.hyperopt', 'worker' +) +if not os.path.exists(WORK_DIR): + os.makedirs(WORK_DIR) + +# Spawn workers +command = [ + 'hyperopt-mongo-worker', + '--mongo=127.0.0.1:1234/{}'.format(DB_NAME), + '--poll-interval=0.1', + '--workdir={}'.format(WORK_DIR), +] +processes = [subprocess.Popen(command) for i in range(PROC_COUNT)] + +# Join all workers +for proc in processes: + proc.wait() diff --git a/scripts/start-mongodb.py b/scripts/start-mongodb.py new file mode 100755 index 000000000..910ee9233 --- /dev/null +++ b/scripts/start-mongodb.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 + +import os +import subprocess + + +DB_PATH = os.path.join( + os.path.sep, + os.path.abspath(os.path.dirname(__file__)), + '..', '.hyperopt', 'mongodb' +) +if not os.path.exists(DB_PATH): + os.makedirs(DB_PATH) + +subprocess.Popen([ + 'mongod', + '--bind_ip=127.0.0.1', + '--port=1234', + '--nohttpinterface', + '--dbpath={}'.format(DB_PATH), +]).wait()