Merge pull request #146 from gcarq/feature/integrate-backtesting
integrate backtesting/hyperopt into freqtrade.optimize
This commit is contained in:
commit
688326b58c
2
.gitignore
vendored
2
.gitignore
vendored
@ -76,6 +76,8 @@ target/
|
||||
config.json
|
||||
preprocessor.py
|
||||
*.sqlite
|
||||
.hyperopt
|
||||
logfile.txt
|
||||
|
||||
.env
|
||||
.venv
|
||||
|
@ -4,9 +4,6 @@ os:
|
||||
language: python
|
||||
python:
|
||||
- 3.6
|
||||
env:
|
||||
- BACKTEST=
|
||||
- BACKTEST=true
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
|
30
README.md
30
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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
41
freqtrade/optimize/__init__.py
Normal file
41
freqtrade/optimize/__init__.py
Normal 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
|
@ -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'])
|
||||
)
|
205
freqtrade/optimize/hyperopt.py
Normal file
205
freqtrade/optimize/hyperopt.py
Normal 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'])
|
@ -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
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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'])
|
@ -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):
|
||||
|
16
freqtrade/tests/test_optimize_backtesting.py
Normal file
16
freqtrade/tests/test_optimize_backtesting.py
Normal 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
|
6
freqtrade/tests/test_optimize_hyperopt.py
Normal file
6
freqtrade/tests/test_optimize_hyperopt.py
Normal file
@ -0,0 +1,6 @@
|
||||
# pragma pylint: disable=missing-docstring,W0212
|
||||
|
||||
|
||||
def test_optimizer(default_conf, mocker):
|
||||
# TODO: implement test
|
||||
pass
|
27
scripts/start-hyperopt-worker.py
Executable file
27
scripts/start-hyperopt-worker.py
Executable 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
21
scripts/start-mongodb.py
Executable 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()
|
Loading…
Reference in New Issue
Block a user