Merge pull request #146 from gcarq/feature/integrate-backtesting

integrate backtesting/hyperopt into freqtrade.optimize
This commit is contained in:
Samuel Husso 2017-11-30 08:19:59 +02:00 committed by GitHub
commit 688326b58c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 462 additions and 339 deletions

2
.gitignore vendored
View File

@ -76,6 +76,8 @@ target/
config.json config.json
preprocessor.py preprocessor.py
*.sqlite *.sqlite
.hyperopt
logfile.txt
.env .env
.venv .venv

View File

@ -4,9 +4,6 @@ os:
language: python language: python
python: python:
- 3.6 - 3.6
env:
- BACKTEST=
- BACKTEST=true
addons: addons:
apt: apt:
packages: packages:

View File

@ -164,25 +164,39 @@ optional arguments:
Backtesting also uses the config specified via `-c/--config`. 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: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
-l, --live using live data -l, --live using live data
-i INT, --ticker-interval INT -i INT, --ticker-interval INT
specify ticker interval in minutes (default: 5) 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 ### Execute tests
``` ```
$ pytest $ pytest freqtrade
```
This will by default skip the slow running backtest set. To run backtest set:
```
$ BACKTEST=true pytest -s freqtrade/tests/test_backtesting.py
``` ```
### Contributing ### Contributing

View File

@ -4,6 +4,7 @@ Functions to analyze ticker data with indicators and produce buy and sell signal
import logging import logging
from datetime import timedelta from datetime import timedelta
from enum import Enum from enum import Enum
from typing import List, Dict
import arrow import arrow
import talib.abstract as ta import talib.abstract as ta
@ -14,6 +15,7 @@ from freqtrade.vendor.qtpylib.indicators import awesome_oscillator, crossed_abov
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class SignalType(Enum): class SignalType(Enum):
""" Enum to distinguish between buy and sell signals """ """ Enum to distinguish between buy and sell signals """
BUY = "buy" BUY = "buy"
@ -113,18 +115,13 @@ def populate_sell_trend(dataframe: DataFrame) -> DataFrame:
return 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 add several TA indicators and buy signal to it
:return DataFrame with ticker data and indicator data :return DataFrame with ticker data and indicator data
""" """
ticker_hist = get_ticker_history(pair) dataframe = parse_ticker_dataframe(ticker_history)
if not ticker_hist:
logger.warning('Empty ticker history for pair %s', pair)
return DataFrame()
dataframe = parse_ticker_dataframe(ticker_hist)
dataframe = populate_indicators(dataframe) dataframe = populate_indicators(dataframe)
dataframe = populate_buy_trend(dataframe) dataframe = populate_buy_trend(dataframe)
dataframe = populate_sell_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 :param pair: pair in format BTC_ANT or BTC-ANT
:return: True if pair is good for buying, False otherwise :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: try:
dataframe = analyze_ticker(pair) dataframe = analyze_ticker(ticker_hist)
except ValueError as ex: except ValueError as ex:
logger.warning('Unable to analyze ticker for pair %s: %s', pair, str(ex)) logger.warning('Unable to analyze ticker for pair %s: %s', pair, str(ex))
return False return False

View File

@ -2,7 +2,6 @@ import argparse
import enum import enum
import json import json
import logging import logging
import os
import time import time
from typing import Any, Callable, List, Dict from typing import Any, Callable, List, Dict
@ -129,16 +128,20 @@ def parse_args(args: List[str]):
def build_subcommands(parser: argparse.ArgumentParser) -> None: def build_subcommands(parser: argparse.ArgumentParser) -> None:
""" Builds and attaches all subcommands """ """ Builds and attaches all subcommands """
from freqtrade.optimize import backtesting, hyperopt
subparsers = parser.add_subparsers(dest='subparser') subparsers = parser.add_subparsers(dest='subparser')
backtest = subparsers.add_parser('backtesting', help='backtesting module')
backtest.set_defaults(func=start_backtesting) # Add backtesting subcommand
backtest.add_argument( backtesting_cmd = subparsers.add_parser('backtesting', help='backtesting module')
backtesting_cmd.set_defaults(func=backtesting.start)
backtesting_cmd.add_argument(
'-l', '--live', '-l', '--live',
action='store_true', action='store_true',
dest='live', dest='live',
help='using live data', help='using live data',
) )
backtest.add_argument( backtesting_cmd.add_argument(
'-i', '--ticker-interval', '-i', '--ticker-interval',
help='specify ticker interval in minutes (default: 5)', help='specify ticker interval in minutes (default: 5)',
dest='ticker_interval', dest='ticker_interval',
@ -146,31 +149,30 @@ def build_subcommands(parser: argparse.ArgumentParser) -> None:
type=int, type=int,
metavar='INT', metavar='INT',
) )
backtest.add_argument( backtesting_cmd.add_argument(
'--realistic-simulation', '--realistic-simulation',
help='uses max_open_trades from config to simulate real world limitations', help='uses max_open_trades from config to simulate real world limitations',
action='store_true', action='store_true',
dest='realistic_simulation', dest='realistic_simulation',
) )
# Add hyperopt subcommand
def start_backtesting(args) -> None: hyperopt_cmd = subparsers.add_parser('hyperopt', help='hyperopt module')
""" hyperopt_cmd.set_defaults(func=hyperopt.start)
Exports all args as environment variables and starts backtesting via pytest. hyperopt_cmd.add_argument(
:param args: arguments namespace '-e', '--epochs',
:return: help='specify number of epochs (default: 100)',
""" dest='epochs',
import pytest default=100,
type=int,
os.environ.update({ metavar='INT',
'BACKTEST': 'true', )
'BACKTEST_LIVE': 'true' if args.live else '', hyperopt_cmd.add_argument(
'BACKTEST_CONFIG': args.config, '--use-mongodb',
'BACKTEST_TICKER_INTERVAL': str(args.ticker_interval), help='parallelize evaluations with mongodb (requires mongod in PATH)',
'BACKTEST_REALISTIC_SIMULATION': 'true' if args.realistic_simulation else '', dest='mongodb',
}) action='store_true',
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

View File

@ -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

View File

@ -2,43 +2,23 @@
import logging import logging
import os
from typing import Tuple, Dict from typing import Tuple, Dict
import arrow import arrow
import pytest
from pandas import DataFrame from pandas import DataFrame
from tabulate import tabulate from tabulate import tabulate
from freqtrade import exchange from freqtrade import exchange
from freqtrade.analyze import parse_ticker_dataframe, populate_indicators, \ from freqtrade.analyze import populate_buy_trend, populate_sell_trend
populate_buy_trend, populate_sell_trend
from freqtrade.exchange import Bittrex from freqtrade.exchange import Bittrex
from freqtrade.main import min_roi_reached from freqtrade.main import min_roi_reached
from freqtrade.misc import load_config from freqtrade.misc import load_config
from freqtrade.optimize import load_data, preprocess
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.tests import load_backtesting_data
logger = logging.getLogger(__name__) 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]: def get_timeframe(data: Dict[str, Dict]) -> Tuple[arrow.Arrow, arrow.Arrow]:
""" """
Get the maximum timeframe for the given backtest data 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) 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 Implements backtesting functionality
:param config: config to use :param config: config to use
:param processed: a processed dictionary with format {pair, data} :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 max_open_trades: maximum number of concurrent trades (default: 0, disabled)
:param realistic: do we try to simulate realistic trades? (default: True) :param realistic: do we try to simulate realistic trades? (default: True)
:return: DataFrame :return: DataFrame
@ -96,7 +76,6 @@ def backtest(config: Dict, processed, mocker, max_open_trades=0, realistic=True)
trades = [] trades = []
trade_count_lock = {} trade_count_lock = {}
exchange._API = Bittrex({'key': '', 'secret': ''}) exchange._API = Bittrex({'key': '', 'secret': ''})
mocker.patch.dict('freqtrade.main._CONF', config)
for pair, pair_data in processed.items(): for pair, pair_data in processed.items():
pair_data['buy'], pair_data['sell'] = 0, 0 pair_data['buy'], pair_data['sell'] = 0, 0
ticker = populate_sell_trend(populate_buy_trend(pair_data)) 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) return DataFrame.from_records(trades, columns=labels)
def get_max_open_trades(config): def start(args):
if not os.environ.get('BACKTEST_REALISTIC_SIMULATION'): # Initialize logger
return 0 logging.basicConfig(
print('Using max_open_trades: {} ...'.format(config['max_open_trades'])) level=args.loglevel,
return config['max_open_trades'] 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': ''}) exchange._API = Bittrex({'key': '', 'secret': ''})
# Load configuration file based on env variable logger.info('Using config: %s ...', args.config)
conf_path = os.environ.get('BACKTEST_CONFIG') config = load_config(args.config)
if conf_path:
print('Using config: {} ...'.format(conf_path))
config = load_config(conf_path)
else:
config = backtest_conf
# Parse ticker interval logger.info('Using ticker_interval: %s ...', args.ticker_interval)
ticker_interval = int(os.environ.get('BACKTEST_TICKER_INTERVAL') or 5)
print('Using ticker_interval: {} ...'.format(ticker_interval))
data = {} data = {}
if os.environ.get('BACKTEST_LIVE'): if args.live:
print('Downloading data for all pairs in whitelist ...') logger.info('Downloading data for all pairs in whitelist ...')
for pair in config['exchange']['pair_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: else:
print('Using local backtesting data (ignoring whitelist in given config)...') logger.info('Using local backtesting data (ignoring whitelist in given config) ...')
data = load_backtesting_data(ticker_interval) data = load_data(args.ticker_interval)
print('Using stake_currency: {} ...\nUsing stake_amount: {} ...'.format( logger.info('Using stake_currency: %s ...', config['stake_currency'])
config['stake_currency'], config['stake_amount'] logger.info('Using stake_amount: %s ...', config['stake_amount'])
))
# Print timeframe # Print timeframe
min_date, max_date = get_timeframe(data) min_date, max_date = get_timeframe(data)
print('Measuring data from {} up to {} ...'.format( logger.info('Measuring data from %s up to %s ...', min_date.isoformat(), max_date.isoformat())
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 # Execute backtest and print results
realistic = os.environ.get('BACKTEST_REALISTIC_SIMULATION') results = backtest(
results = backtest(config, preprocess(data), mocker, get_max_open_trades(config), realistic) config, preprocess(data), max_open_trades, args.realistic_simulation
print('====================== BACKTESTING REPORT ======================================\n\n') )
print(generate_text_table(data, results, config['stake_currency'])) logger.info(
'\n====================== BACKTESTING REPORT ======================================\n%s',
generate_text_table(data, results, config['stake_currency'])
)

View File

@ -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'])

View File

@ -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

View File

@ -51,22 +51,6 @@ def default_conf():
return configuration 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 @pytest.fixture
def update(): def update():
_update = Update(0) _update = Update(0)

View File

@ -1,5 +1,6 @@
# pragma pylint: disable=missing-docstring,W0621 # pragma pylint: disable=missing-docstring,W0621
import json import json
from unittest.mock import MagicMock
import arrow import arrow
import pytest import pytest
@ -35,20 +36,30 @@ def test_populates_sell_trend(result):
def test_returns_latest_buy_signal(mocker): def test_returns_latest_buy_signal(mocker):
buydf = DataFrame([{'buy': 1, 'date': arrow.utcnow()}]) mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock())
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf) mocker.patch(
'freqtrade.analyze.analyze_ticker',
return_value=DataFrame([{'buy': 1, 'date': arrow.utcnow()}])
)
assert get_signal('BTC-ETH', SignalType.BUY) assert get_signal('BTC-ETH', SignalType.BUY)
buydf = DataFrame([{'buy': 0, 'date': arrow.utcnow()}]) mocker.patch(
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf) 'freqtrade.analyze.analyze_ticker',
return_value=DataFrame([{'buy': 0, 'date': arrow.utcnow()}])
)
assert not get_signal('BTC-ETH', SignalType.BUY) assert not get_signal('BTC-ETH', SignalType.BUY)
def test_returns_latest_sell_signal(mocker): def test_returns_latest_sell_signal(mocker):
selldf = DataFrame([{'sell': 1, 'date': arrow.utcnow()}]) mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock())
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=selldf) mocker.patch(
'freqtrade.analyze.analyze_ticker',
return_value=DataFrame([{'sell': 1, 'date': arrow.utcnow()}])
)
assert get_signal('BTC-ETH', SignalType.SELL) assert get_signal('BTC-ETH', SignalType.SELL)
selldf = DataFrame([{'sell': 0, 'date': arrow.utcnow()}]) mocker.patch(
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=selldf) 'freqtrade.analyze.analyze_ticker',
return_value=DataFrame([{'sell': 0, 'date': arrow.utcnow()}])
)
assert not get_signal('BTC-ETH', SignalType.SELL) assert not get_signal('BTC-ETH', SignalType.SELL)

View File

@ -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'])

View File

@ -1,15 +1,13 @@
# pragma pylint: disable=missing-docstring,C0103 # pragma pylint: disable=missing-docstring,C0103
import json import json
import os
import time import time
from argparse import Namespace
from copy import deepcopy from copy import deepcopy
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pytest import pytest
from jsonschema import ValidationError 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(): def test_throttle():
@ -64,7 +62,7 @@ def test_parse_args_dynamic_whitelist():
def test_parse_args_backtesting(mocker): 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']) args = parse_args(['backtesting'])
assert args is None assert args is None
assert backtesting_mock.call_count == 1 assert backtesting_mock.call_count == 1
@ -80,14 +78,14 @@ def test_parse_args_backtesting(mocker):
def test_parse_args_backtesting_invalid(): def test_parse_args_backtesting_invalid():
with pytest.raises(SystemExit, match=r'2'): with pytest.raises(SystemExit, match=r'2'):
parse_args(['--ticker-interval']) parse_args(['backtesting --ticker-interval'])
with pytest.raises(SystemExit, match=r'2'): 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): 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']) args = parse_args(['-c', 'test_conf.json', 'backtesting', '--live', '--ticker-interval', '1'])
assert args is None assert args is None
assert backtesting_mock.call_count == 1 assert backtesting_mock.call_count == 1
@ -101,29 +99,31 @@ def test_parse_args_backtesting_custom(mocker):
assert call_args.ticker_interval == 1 assert call_args.ticker_interval == 1
def test_start_backtesting(mocker): def test_parse_args_hyperopt(mocker):
pytest_mock = mocker.patch('pytest.main', MagicMock()) hyperopt_mock = mocker.patch('freqtrade.optimize.hyperopt.start', MagicMock())
env_mock = mocker.patch('os.environ', {}) args = parse_args(['hyperopt'])
args = Namespace( assert args is None
config='config.json', assert hyperopt_mock.call_count == 1
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] call_args = hyperopt_mock.call_args[0][0]
assert main_call_args[0] == '-s' assert call_args.config == 'config.json'
assert main_call_args[1].endswith(os.path.join('freqtrade', 'tests', 'test_backtesting.py')) 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): def test_load_config(default_conf, mocker):

View File

@ -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

View File

@ -0,0 +1,6 @@
# pragma pylint: disable=missing-docstring,W0212
def test_optimizer(default_conf, mocker):
# TODO: implement test
pass

View File

@ -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()

21
scripts/start-mongodb.py Executable file
View File

@ -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()