1994 lines
80 KiB
Python
1994 lines
80 KiB
Python
# pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, unused-argument
|
|
|
|
import random
|
|
from copy import deepcopy
|
|
from datetime import datetime, timedelta, timezone
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, PropertyMock
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
import pytest
|
|
from arrow import Arrow
|
|
|
|
from freqtrade import constants
|
|
from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_backtesting
|
|
from freqtrade.configuration import TimeRange
|
|
from freqtrade.data import history
|
|
from freqtrade.data.btanalysis import BT_DATA_COLUMNS, evaluate_result_multi
|
|
from freqtrade.data.converter import clean_ohlcv_dataframe
|
|
from freqtrade.data.dataprovider import DataProvider
|
|
from freqtrade.data.history import get_timerange
|
|
from freqtrade.enums import CandleType, ExitType, RunMode
|
|
from freqtrade.exceptions import DependencyException, OperationalException
|
|
from freqtrade.exchange.exchange import timeframe_to_next_date
|
|
from freqtrade.optimize.backtest_caching import get_strategy_run_id
|
|
from freqtrade.optimize.backtesting import Backtesting
|
|
from freqtrade.persistence import LocalTrade, Trade
|
|
from freqtrade.resolvers import StrategyResolver
|
|
from tests.conftest import (CURRENT_TEST_STRATEGY, EXMS, get_args, log_has, log_has_re,
|
|
patch_exchange, patched_configuration_load_config_file)
|
|
|
|
|
|
ORDER_TYPES = [
|
|
{
|
|
'entry': 'limit',
|
|
'exit': 'limit',
|
|
'stoploss': 'limit',
|
|
'stoploss_on_exchange': False
|
|
},
|
|
{
|
|
'entry': 'limit',
|
|
'exit': 'limit',
|
|
'stoploss': 'limit',
|
|
'stoploss_on_exchange': True
|
|
}]
|
|
|
|
|
|
def trim_dictlist(dict_list, num):
|
|
new = {}
|
|
for pair, pair_data in dict_list.items():
|
|
new[pair] = pair_data[num:].reset_index()
|
|
return new
|
|
|
|
|
|
def load_data_test(what, testdatadir):
|
|
timerange = TimeRange.parse_timerange('1510694220-1510700340')
|
|
data = history.load_pair_history(pair='UNITTEST/BTC', datadir=testdatadir,
|
|
timeframe='1m', timerange=timerange,
|
|
drop_incomplete=False,
|
|
fill_up_missing=False)
|
|
|
|
base = 0.001
|
|
if what == 'raise':
|
|
data.loc[:, 'open'] = data.index * base
|
|
data.loc[:, 'high'] = data.index * base + 0.0001
|
|
data.loc[:, 'low'] = data.index * base - 0.0001
|
|
data.loc[:, 'close'] = data.index * base
|
|
|
|
if what == 'lower':
|
|
data.loc[:, 'open'] = 1 - data.index * base
|
|
data.loc[:, 'high'] = 1 - data.index * base + 0.0001
|
|
data.loc[:, 'low'] = 1 - data.index * base - 0.0001
|
|
data.loc[:, 'close'] = 1 - data.index * base
|
|
|
|
if what == 'sine':
|
|
hz = 0.1 # frequency
|
|
data.loc[:, 'open'] = np.sin(data.index * hz) / 1000 + base
|
|
data.loc[:, 'high'] = np.sin(data.index * hz) / 1000 + base + 0.0001
|
|
data.loc[:, 'low'] = np.sin(data.index * hz) / 1000 + base - 0.0001
|
|
data.loc[:, 'close'] = np.sin(data.index * hz) / 1000 + base
|
|
|
|
return {'UNITTEST/BTC': clean_ohlcv_dataframe(data, timeframe='1m', pair='UNITTEST/BTC',
|
|
fill_missing=True, drop_incomplete=True)}
|
|
|
|
|
|
# FIX: fixturize this?
|
|
def _make_backtest_conf(mocker, datadir, conf=None, pair='UNITTEST/BTC'):
|
|
data = history.load_data(datadir=datadir, timeframe='1m', pairs=[pair])
|
|
data = trim_dictlist(data, -201)
|
|
patch_exchange(mocker)
|
|
backtesting = Backtesting(conf)
|
|
backtesting._set_strategy(backtesting.strategylist[0])
|
|
processed = backtesting.strategy.advise_all_indicators(data)
|
|
min_date, max_date = get_timerange(processed)
|
|
return {
|
|
'processed': processed,
|
|
'start_date': min_date,
|
|
'end_date': max_date,
|
|
}
|
|
|
|
|
|
def _trend(signals, buy_value, sell_value):
|
|
n = len(signals['low'])
|
|
buy = np.zeros(n)
|
|
sell = np.zeros(n)
|
|
for i in range(0, len(signals['date'])):
|
|
if random.random() > 0.5: # Both buy and sell signals at same timeframe
|
|
buy[i] = buy_value
|
|
sell[i] = sell_value
|
|
signals['enter_long'] = buy
|
|
signals['exit_long'] = sell
|
|
signals['enter_short'] = 0
|
|
signals['exit_short'] = 0
|
|
return signals
|
|
|
|
|
|
def _trend_alternate(dataframe=None, metadata=None):
|
|
signals = dataframe
|
|
low = signals['low']
|
|
n = len(low)
|
|
buy = np.zeros(n)
|
|
sell = np.zeros(n)
|
|
for i in range(0, len(buy)):
|
|
if i % 2 == 0:
|
|
buy[i] = 1
|
|
else:
|
|
sell[i] = 1
|
|
signals['enter_long'] = buy
|
|
signals['exit_long'] = sell
|
|
signals['enter_short'] = 0
|
|
signals['exit_short'] = 0
|
|
return dataframe
|
|
|
|
|
|
# Unit tests
|
|
def test_setup_optimize_configuration_without_arguments(mocker, default_conf, caplog) -> None:
|
|
patched_configuration_load_config_file(mocker, default_conf)
|
|
|
|
args = [
|
|
'backtesting',
|
|
'--config', 'config.json',
|
|
'--strategy', CURRENT_TEST_STRATEGY,
|
|
'--export', 'none'
|
|
]
|
|
|
|
config = setup_optimize_configuration(get_args(args), RunMode.BACKTEST)
|
|
assert 'max_open_trades' in config
|
|
assert 'stake_currency' in config
|
|
assert 'stake_amount' in config
|
|
assert 'exchange' in config
|
|
assert 'pair_whitelist' in config['exchange']
|
|
assert 'datadir' in config
|
|
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
|
|
assert 'timeframe' in config
|
|
assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog)
|
|
|
|
assert 'position_stacking' not in config
|
|
assert not log_has('Parameter --enable-position-stacking detected ...', caplog)
|
|
|
|
assert 'timerange' not in config
|
|
assert 'export' in config
|
|
assert config['export'] == 'none'
|
|
assert 'runmode' in config
|
|
assert config['runmode'] == RunMode.BACKTEST
|
|
|
|
|
|
def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> None:
|
|
patched_configuration_load_config_file(mocker, default_conf)
|
|
mocker.patch(
|
|
'freqtrade.configuration.configuration.create_datadir',
|
|
lambda c, x: x
|
|
)
|
|
|
|
args = [
|
|
'backtesting',
|
|
'--config', 'config.json',
|
|
'--strategy', CURRENT_TEST_STRATEGY,
|
|
'--datadir', '/foo/bar',
|
|
'--timeframe', '1m',
|
|
'--enable-position-stacking',
|
|
'--disable-max-market-positions',
|
|
'--timerange', ':100',
|
|
'--export-filename', 'foo_bar.json',
|
|
'--fee', '0',
|
|
]
|
|
|
|
config = setup_optimize_configuration(get_args(args), RunMode.BACKTEST)
|
|
assert 'max_open_trades' in config
|
|
assert 'stake_currency' in config
|
|
assert 'stake_amount' in config
|
|
assert 'exchange' in config
|
|
assert 'pair_whitelist' in config['exchange']
|
|
assert 'datadir' in config
|
|
assert config['runmode'] == RunMode.BACKTEST
|
|
|
|
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
|
|
assert 'timeframe' in config
|
|
assert log_has('Parameter -i/--timeframe detected ... Using timeframe: 1m ...',
|
|
caplog)
|
|
|
|
assert 'position_stacking' in config
|
|
assert log_has('Parameter --enable-position-stacking detected ...', caplog)
|
|
|
|
assert 'use_max_market_positions' in config
|
|
assert log_has('Parameter --disable-max-market-positions detected ...', caplog)
|
|
assert log_has('max_open_trades set to unlimited ...', caplog)
|
|
|
|
assert 'timerange' in config
|
|
assert log_has('Parameter --timerange detected: {} ...'.format(config['timerange']), caplog)
|
|
|
|
assert 'export' in config
|
|
assert 'exportfilename' in config
|
|
assert isinstance(config['exportfilename'], Path)
|
|
assert log_has('Storing backtest results to {} ...'.format(config['exportfilename']), caplog)
|
|
|
|
assert 'fee' in config
|
|
assert log_has('Parameter --fee detected, setting fee to: {} ...'.format(config['fee']), caplog)
|
|
|
|
|
|
def test_setup_optimize_configuration_stake_amount(mocker, default_conf, caplog) -> None:
|
|
|
|
patched_configuration_load_config_file(mocker, default_conf)
|
|
|
|
args = [
|
|
'backtesting',
|
|
'--config', 'config.json',
|
|
'--strategy', CURRENT_TEST_STRATEGY,
|
|
'--stake-amount', '1',
|
|
'--starting-balance', '2'
|
|
]
|
|
|
|
conf = setup_optimize_configuration(get_args(args), RunMode.BACKTEST)
|
|
assert isinstance(conf, dict)
|
|
|
|
args = [
|
|
'backtesting',
|
|
'--config', 'config.json',
|
|
'--strategy', CURRENT_TEST_STRATEGY,
|
|
'--stake-amount', '1',
|
|
'--starting-balance', '0.5'
|
|
]
|
|
with pytest.raises(OperationalException, match=r"Starting balance .* smaller .*"):
|
|
setup_optimize_configuration(get_args(args), RunMode.BACKTEST)
|
|
|
|
|
|
def test_start(mocker, fee, default_conf, caplog) -> None:
|
|
start_mock = MagicMock()
|
|
mocker.patch(f'{EXMS}.get_fee', fee)
|
|
patch_exchange(mocker)
|
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.start', start_mock)
|
|
patched_configuration_load_config_file(mocker, default_conf)
|
|
|
|
args = [
|
|
'backtesting',
|
|
'--config', 'config.json',
|
|
'--strategy', CURRENT_TEST_STRATEGY,
|
|
]
|
|
pargs = get_args(args)
|
|
start_backtesting(pargs)
|
|
assert log_has('Starting freqtrade in Backtesting mode', caplog)
|
|
assert start_mock.call_count == 1
|
|
|
|
|
|
@pytest.mark.parametrize("order_types", ORDER_TYPES)
|
|
def test_backtesting_init(mocker, default_conf, order_types) -> None:
|
|
"""
|
|
Check that stoploss_on_exchange is set to False while backtesting
|
|
since backtesting assumes a perfect stoploss anyway.
|
|
"""
|
|
default_conf["order_types"] = order_types
|
|
patch_exchange(mocker)
|
|
get_fee = mocker.patch(f'{EXMS}.get_fee', MagicMock(return_value=0.5))
|
|
backtesting = Backtesting(default_conf)
|
|
backtesting._set_strategy(backtesting.strategylist[0])
|
|
assert backtesting.config == default_conf
|
|
assert backtesting.timeframe == '5m'
|
|
assert callable(backtesting.strategy.advise_all_indicators)
|
|
assert callable(backtesting.strategy.advise_entry)
|
|
assert callable(backtesting.strategy.advise_exit)
|
|
assert isinstance(backtesting.strategy.dp, DataProvider)
|
|
get_fee.assert_called()
|
|
assert backtesting.fee == 0.5
|
|
assert not backtesting.strategy.order_types["stoploss_on_exchange"]
|
|
assert backtesting.strategy.bot_started is True
|
|
|
|
|
|
def test_backtesting_init_no_timeframe(mocker, default_conf, caplog) -> None:
|
|
patch_exchange(mocker)
|
|
del default_conf['timeframe']
|
|
default_conf['strategy_list'] = [CURRENT_TEST_STRATEGY,
|
|
'HyperoptableStrategy']
|
|
|
|
mocker.patch(f'{EXMS}.get_fee', MagicMock(return_value=0.5))
|
|
with pytest.raises(OperationalException,
|
|
match=r"Timeframe needs to be set in either configuration"):
|
|
Backtesting(default_conf)
|
|
|
|
|
|
def test_data_with_fee(default_conf, mocker) -> None:
|
|
patch_exchange(mocker)
|
|
default_conf['fee'] = 0.1234
|
|
|
|
fee_mock = mocker.patch(f'{EXMS}.get_fee', MagicMock(return_value=0.5))
|
|
backtesting = Backtesting(default_conf)
|
|
backtesting._set_strategy(backtesting.strategylist[0])
|
|
assert backtesting.fee == 0.1234
|
|
assert fee_mock.call_count == 0
|
|
|
|
default_conf['fee'] = 0.0
|
|
backtesting = Backtesting(default_conf)
|
|
backtesting._set_strategy(backtesting.strategylist[0])
|
|
assert backtesting.fee == 0.0
|
|
assert fee_mock.call_count == 0
|
|
|
|
|
|
def test_data_to_dataframe_bt(default_conf, mocker, testdatadir) -> None:
|
|
patch_exchange(mocker)
|
|
timerange = TimeRange.parse_timerange('1510694220-1510700340')
|
|
data = history.load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange,
|
|
fill_up_missing=True)
|
|
backtesting = Backtesting(default_conf)
|
|
backtesting._set_strategy(backtesting.strategylist[0])
|
|
processed = backtesting.strategy.advise_all_indicators(data)
|
|
assert len(processed['UNITTEST/BTC']) == 103
|
|
|
|
# Load strategy to compare the result between Backtesting function and strategy are the same
|
|
strategy = StrategyResolver.load_strategy(default_conf)
|
|
|
|
processed2 = strategy.advise_all_indicators(data)
|
|
assert processed['UNITTEST/BTC'].equals(processed2['UNITTEST/BTC'])
|
|
|
|
|
|
def test_backtest_abort(default_conf, mocker, testdatadir) -> None:
|
|
patch_exchange(mocker)
|
|
backtesting = Backtesting(default_conf)
|
|
backtesting.check_abort()
|
|
|
|
backtesting.abort = True
|
|
|
|
with pytest.raises(DependencyException, match="Stop requested"):
|
|
backtesting.check_abort()
|
|
# abort flag resets
|
|
assert backtesting.abort is False
|
|
assert backtesting.progress.progress == 0
|
|
|
|
|
|
def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None:
|
|
def get_timerange(input1):
|
|
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
|
|
|
|
mocker.patch('freqtrade.data.history.get_timerange', get_timerange)
|
|
patch_exchange(mocker)
|
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest')
|
|
mocker.patch('freqtrade.optimize.backtesting.generate_backtest_stats')
|
|
mocker.patch('freqtrade.optimize.backtesting.show_backtest_results')
|
|
sbs = mocker.patch('freqtrade.optimize.backtesting.store_backtest_stats')
|
|
sbc = mocker.patch('freqtrade.optimize.backtesting.store_backtest_signal_candles')
|
|
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
|
|
PropertyMock(return_value=['UNITTEST/BTC']))
|
|
|
|
default_conf['timeframe'] = '1m'
|
|
default_conf['export'] = 'signals'
|
|
default_conf['exportfilename'] = 'export.txt'
|
|
default_conf['timerange'] = '-1510694220'
|
|
default_conf['runmode'] = RunMode.BACKTEST
|
|
|
|
backtesting = Backtesting(default_conf)
|
|
backtesting._set_strategy(backtesting.strategylist[0])
|
|
backtesting.strategy.bot_loop_start = MagicMock()
|
|
backtesting.start()
|
|
# check the logs, that will contain the backtest result
|
|
exists = [
|
|
'Backtesting with data from 2017-11-14 21:17:00 '
|
|
'up to 2017-11-14 22:59:00 (0 days).'
|
|
]
|
|
for line in exists:
|
|
assert log_has(line, caplog)
|
|
assert backtesting.strategy.dp._pairlists is not None
|
|
assert backtesting.strategy.bot_loop_start.call_count == 1
|
|
assert sbs.call_count == 1
|
|
assert sbc.call_count == 1
|
|
|
|
|
|
def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> None:
|
|
def get_timerange(input1):
|
|
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
|
|
|
|
mocker.patch('freqtrade.data.history.history_utils.load_pair_history',
|
|
MagicMock(return_value=pd.DataFrame()))
|
|
mocker.patch('freqtrade.data.history.get_timerange', get_timerange)
|
|
patch_exchange(mocker)
|
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest')
|
|
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
|
|
PropertyMock(return_value=['UNITTEST/BTC']))
|
|
|
|
default_conf['timeframe'] = "1m"
|
|
default_conf['export'] = 'none'
|
|
default_conf['timerange'] = '20180101-20180102'
|
|
|
|
backtesting = Backtesting(default_conf)
|
|
backtesting._set_strategy(backtesting.strategylist[0])
|
|
with pytest.raises(OperationalException, match='No data found. Terminating.'):
|
|
backtesting.start()
|
|
|
|
|
|
def test_backtesting_no_pair_left(default_conf, mocker, caplog, testdatadir) -> None:
|
|
mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True))
|
|
mocker.patch('freqtrade.data.history.history_utils.load_pair_history',
|
|
MagicMock(return_value=pd.DataFrame()))
|
|
mocker.patch('freqtrade.data.history.get_timerange', get_timerange)
|
|
patch_exchange(mocker)
|
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest')
|
|
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
|
|
PropertyMock(return_value=[]))
|
|
|
|
default_conf['timeframe'] = "1m"
|
|
default_conf['export'] = 'none'
|
|
default_conf['timerange'] = '20180101-20180102'
|
|
|
|
with pytest.raises(OperationalException, match='No pair in whitelist.'):
|
|
Backtesting(default_conf)
|
|
|
|
default_conf['pairlists'] = [{"method": "VolumePairList", "number_assets": 5}]
|
|
with pytest.raises(OperationalException,
|
|
match=r'VolumePairList not allowed for backtesting\..*StaticPairList.*'):
|
|
Backtesting(default_conf)
|
|
|
|
default_conf.update({
|
|
'pairlists': [{"method": "StaticPairList"}],
|
|
'timeframe_detail': '1d',
|
|
})
|
|
|
|
with pytest.raises(OperationalException,
|
|
match='Detail timeframe must be smaller than strategy timeframe.'):
|
|
Backtesting(default_conf)
|
|
|
|
|
|
def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, tickers) -> None:
|
|
mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True))
|
|
mocker.patch(f'{EXMS}.get_tickers', tickers)
|
|
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
|
|
mocker.patch('freqtrade.data.history.get_timerange', get_timerange)
|
|
patch_exchange(mocker)
|
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest')
|
|
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
|
|
PropertyMock(return_value=['XRP/BTC']))
|
|
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.refresh_pairlist')
|
|
|
|
default_conf['ticker_interval'] = "1m"
|
|
default_conf['export'] = 'none'
|
|
# Use stoploss from strategy
|
|
del default_conf['stoploss']
|
|
default_conf['timerange'] = '20180101-20180102'
|
|
|
|
default_conf['pairlists'] = [{"method": "VolumePairList", "number_assets": 5}]
|
|
with pytest.raises(OperationalException,
|
|
match=r'VolumePairList not allowed for backtesting\..*StaticPairList.*'):
|
|
Backtesting(default_conf)
|
|
|
|
default_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PerformanceFilter"}]
|
|
with pytest.raises(OperationalException,
|
|
match='PerformanceFilter not allowed for backtesting.'):
|
|
Backtesting(default_conf)
|
|
|
|
default_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PrecisionFilter"}, ]
|
|
Backtesting(default_conf)
|
|
|
|
# Multiple strategies
|
|
default_conf['strategy_list'] = [CURRENT_TEST_STRATEGY, 'StrategyTestV2']
|
|
with pytest.raises(OperationalException,
|
|
match='PrecisionFilter not allowed for backtesting multiple strategies.'):
|
|
Backtesting(default_conf)
|
|
|
|
|
|
def test_backtest__enter_trade(default_conf, fee, mocker) -> None:
|
|
default_conf['use_exit_signal'] = False
|
|
mocker.patch(f'{EXMS}.get_fee', fee)
|
|
mocker.patch(f'{EXMS}.get_min_pair_stake_amount', return_value=0.00001)
|
|
mocker.patch(f'{EXMS}.get_max_pair_stake_amount', return_value=float('inf'))
|
|
patch_exchange(mocker)
|
|
default_conf['stake_amount'] = 'unlimited'
|
|
default_conf['max_open_trades'] = 2
|
|
backtesting = Backtesting(default_conf)
|
|
backtesting._set_strategy(backtesting.strategylist[0])
|
|
pair = 'UNITTEST/BTC'
|
|
row = [
|
|
pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0),
|
|
1, # Buy
|
|
0.001, # Open
|
|
0.0011, # Close
|
|
0, # Sell
|
|
0.00099, # Low
|
|
0.0012, # High
|
|
'', # Buy Signal Name
|
|
]
|
|
trade = backtesting._enter_trade(pair, row=row, direction='long')
|
|
assert isinstance(trade, LocalTrade)
|
|
assert trade.stake_amount == 495
|
|
|
|
# Fake 2 trades, so there's not enough amount for the next trade left.
|
|
LocalTrade.trades_open.append(trade)
|
|
LocalTrade.trades_open.append(trade)
|
|
backtesting.wallets.update()
|
|
trade = backtesting._enter_trade(pair, row=row, direction='long')
|
|
assert trade is None
|
|
LocalTrade.trades_open.pop()
|
|
trade = backtesting._enter_trade(pair, row=row, direction='long')
|
|
assert trade is not None
|
|
|
|
backtesting.strategy.custom_stake_amount = lambda **kwargs: 123.5
|
|
backtesting.wallets.update()
|
|
trade = backtesting._enter_trade(pair, row=row, direction='long')
|
|
assert trade
|
|
assert trade.stake_amount == 123.5
|
|
|
|
# In case of error - use proposed stake
|
|
backtesting.strategy.custom_stake_amount = lambda **kwargs: 20 / 0
|
|
trade = backtesting._enter_trade(pair, row=row, direction='long')
|
|
assert trade
|
|
assert trade.stake_amount == 495
|
|
assert trade.is_short is False
|
|
|
|
trade = backtesting._enter_trade(pair, row=row, direction='short')
|
|
assert trade
|
|
assert trade.stake_amount == 495
|
|
assert trade.is_short is True
|
|
|
|
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=300.0)
|
|
trade = backtesting._enter_trade(pair, row=row, direction='long')
|
|
assert trade
|
|
assert trade.stake_amount == 300.0
|
|
|
|
|
|
def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None:
|
|
default_conf_usdt['use_exit_signal'] = False
|
|
mocker.patch(f'{EXMS}.get_fee', fee)
|
|
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
|
|
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf'))
|
|
mocker.patch(f"{EXMS}.get_max_leverage", return_value=100)
|
|
mocker.patch("freqtrade.optimize.backtesting.price_to_precision", lambda p, *args: p)
|
|
patch_exchange(mocker)
|
|
default_conf_usdt['stake_amount'] = 300
|
|
default_conf_usdt['max_open_trades'] = 2
|
|
default_conf_usdt['trading_mode'] = 'futures'
|
|
default_conf_usdt['margin_mode'] = 'isolated'
|
|
default_conf_usdt['stake_currency'] = 'USDT'
|
|
default_conf_usdt['exchange']['pair_whitelist'] = ['.*']
|
|
backtesting = Backtesting(default_conf_usdt)
|
|
backtesting._set_strategy(backtesting.strategylist[0])
|
|
pair = 'ETH/USDT:USDT'
|
|
row = [
|
|
pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0),
|
|
0.1, # Open
|
|
0.12, # High
|
|
0.099, # Low
|
|
0.11, # Close
|
|
1, # enter_long
|
|
0, # exit_long
|
|
1, # enter_short
|
|
0, # exit_hsort
|
|
'', # Long Signal Name
|
|
'', # Short Signal Name
|
|
'', # Exit Signal Name
|
|
]
|
|
|
|
backtesting.strategy.leverage = MagicMock(return_value=5.0)
|
|
mocker.patch(f"{EXMS}.get_maintenance_ratio_and_amt",
|
|
return_value=(0.01, 0.01))
|
|
|
|
# leverage = 5
|
|
# ep1(trade.open_rate) = 0.1
|
|
# position(trade.amount) = 15000
|
|
# stake_amount = 300 -> wb = 300 / 5 = 60
|
|
# mmr = 0.01
|
|
# cum_b = 0.01
|
|
# side_1: -1 if is_short else 1
|
|
# liq_buffer = 0.05
|
|
#
|
|
# Binance, Long
|
|
# liquidation_price
|
|
# = ((wb + cum_b) - (side_1 * position * ep1)) / ((position * mmr_b) - (side_1 * position))
|
|
# = ((300 + 0.01) - (1 * 15000 * 0.1)) / ((15000 * 0.01) - (1 * 15000))
|
|
# = 0.0008080740740740741
|
|
# freqtrade_liquidation_price = liq + (abs(open_rate - liq) * liq_buffer * side_1)
|
|
# = 0.08080740740740741 + ((0.1 - 0.08080740740740741) * 0.05 * 1)
|
|
# = 0.08176703703703704
|
|
|
|
trade = backtesting._enter_trade(pair, row=row, direction='long')
|
|
assert pytest.approx(trade.liquidation_price) == 0.081767037
|
|
|
|
# Binance, Short
|
|
# liquidation_price
|
|
# = ((wb + cum_b) - (side_1 * position * ep1)) / ((position * mmr_b) - (side_1 * position))
|
|
# = ((300 + 0.01) - ((-1) * 15000 * 0.1)) / ((15000 * 0.01) - ((-1) * 15000))
|
|
# = 0.0011881254125412541
|
|
# freqtrade_liquidation_price = liq + (abs(open_rate - liq) * liq_buffer * side_1)
|
|
# = 0.11881254125412541 + (abs(0.1 - 0.11881254125412541) * 0.05 * -1)
|
|
# = 0.11787191419141915
|
|
|
|
trade = backtesting._enter_trade(pair, row=row, direction='short')
|
|
assert pytest.approx(trade.liquidation_price) == 0.11787191
|
|
|
|
# Stake-amount too high!
|
|
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=600.0)
|
|
|
|
trade = backtesting._enter_trade(pair, row=row, direction='long')
|
|
assert trade is None
|
|
|
|
# Stake-amount throwing error
|
|
mocker.patch("freqtrade.wallets.Wallets.get_trade_stake_amount",
|
|
side_effect=DependencyException)
|
|
|
|
trade = backtesting._enter_trade(pair, row=row, direction='long')
|
|
assert trade is None
|
|
|
|
|
|
def test_backtest__check_trade_exit(default_conf, fee, mocker) -> None:
|
|
default_conf['use_exit_signal'] = False
|
|
mocker.patch(f'{EXMS}.get_fee', fee)
|
|
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
|
|
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf'))
|
|
patch_exchange(mocker)
|
|
default_conf['timeframe_detail'] = '1m'
|
|
default_conf['max_open_trades'] = 2
|
|
backtesting = Backtesting(default_conf)
|
|
backtesting._set_strategy(backtesting.strategylist[0])
|
|
pair = 'UNITTEST/BTC'
|
|
row = [
|
|
pd.Timestamp(year=2020, month=1, day=1, hour=4, minute=55, tzinfo=timezone.utc),
|
|
200, # Open
|
|
201.5, # High
|
|
195, # Low
|
|
201, # Close
|
|
1, # enter_long
|
|
0, # exit_long
|
|
0, # enter_short
|
|
0, # exit_hsort
|
|
'', # Long Signal Name
|
|
'', # Short Signal Name
|
|
'', # Exit Signal Name
|
|
]
|
|
|
|
trade = backtesting._enter_trade(pair, row=row, direction='long')
|
|
assert isinstance(trade, LocalTrade)
|
|
|
|
row_sell = [
|
|
pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0, tzinfo=timezone.utc),
|
|
200, # Open
|
|
210.5, # High
|
|
195, # Low
|
|
201, # Close
|
|
0, # enter_long
|
|
0, # exit_long
|
|
0, # enter_short
|
|
0, # exit_short
|
|
'', # long Signal Name
|
|
'', # Short Signal Name
|
|
'', # Exit Signal Name
|
|
|
|
]
|
|
|
|
# No data available.
|
|
res = backtesting._check_trade_exit(trade, row_sell)
|
|
assert res is not None
|
|
assert res.exit_reason == ExitType.ROI.value
|
|
assert res.close_date_utc == datetime(2020, 1, 1, 5, 0, tzinfo=timezone.utc)
|
|
|
|
# Enter new trade
|
|
trade = backtesting._enter_trade(pair, row=row, direction='long')
|
|
assert isinstance(trade, LocalTrade)
|
|
# Assign empty ... no result.
|
|
backtesting.detail_data[pair] = pd.DataFrame(
|
|
[], columns=['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long',
|
|
'enter_short', 'exit_short', 'long_tag', 'short_tag', 'exit_tag'])
|
|
|
|
res = backtesting._check_trade_exit(trade, row)
|
|
assert res is None
|
|
|
|
|
|
def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
|
|
default_conf['use_exit_signal'] = False
|
|
default_conf['max_open_trades'] = 10
|
|
|
|
mocker.patch(f'{EXMS}.get_fee', fee)
|
|
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
|
|
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf'))
|
|
patch_exchange(mocker)
|
|
backtesting = Backtesting(default_conf)
|
|
backtesting._set_strategy(backtesting.strategylist[0])
|
|
pair = 'UNITTEST/BTC'
|
|
timerange = TimeRange('date', None, 1517227800, 0)
|
|
data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'],
|
|
timerange=timerange)
|
|
processed = backtesting.strategy.advise_all_indicators(data)
|
|
min_date, max_date = get_timerange(processed)
|
|
|
|
result = backtesting.backtest(
|
|
processed=deepcopy(processed),
|
|
start_date=min_date,
|
|
end_date=max_date,
|
|
)
|
|
results = result['results']
|
|
assert not results.empty
|
|
assert len(results) == 2
|
|
|
|
expected = pd.DataFrame(
|
|
{'pair': [pair, pair],
|
|
'stake_amount': [0.001, 0.001],
|
|
'max_stake_amount': [0.001, 0.001],
|
|
'amount': [0.00957442, 0.0097064],
|
|
'open_date': pd.to_datetime([Arrow(2018, 1, 29, 18, 40, 0).datetime,
|
|
Arrow(2018, 1, 30, 3, 30, 0).datetime], utc=True
|
|
),
|
|
'close_date': pd.to_datetime([Arrow(2018, 1, 29, 22, 35, 0).datetime,
|
|
Arrow(2018, 1, 30, 4, 10, 0).datetime], utc=True),
|
|
'open_rate': [0.104445, 0.10302485],
|
|
'close_rate': [0.104969, 0.103541],
|
|
'fee_open': [0.0025, 0.0025],
|
|
'fee_close': [0.0025, 0.0025],
|
|
'trade_duration': [235, 40],
|
|
'profit_ratio': [0.0, 0.0],
|
|
'profit_abs': [0.0, 0.0],
|
|
'exit_reason': [ExitType.ROI.value, ExitType.ROI.value],
|
|
'initial_stop_loss_abs': [0.0940005, 0.09272236],
|
|
'initial_stop_loss_ratio': [-0.1, -0.1],
|
|
'stop_loss_abs': [0.0940005, 0.09272236],
|
|
'stop_loss_ratio': [-0.1, -0.1],
|
|
'min_rate': [0.10370188, 0.10300000000000001],
|
|
'max_rate': [0.10501, 0.1038888],
|
|
'is_open': [False, False],
|
|
'enter_tag': [None, None],
|
|
"leverage": [1.0, 1.0],
|
|
"is_short": [False, False],
|
|
'open_timestamp': [1517251200000, 1517283000000],
|
|
'close_timestamp': [1517265300000, 1517285400000],
|
|
'orders': [
|
|
[
|
|
{'amount': 0.00957442, 'safe_price': 0.104445, 'ft_order_side': 'buy',
|
|
'order_filled_timestamp': 1517251200000, 'ft_is_entry': True},
|
|
{'amount': 0.00957442, 'safe_price': 0.10496853383458644, 'ft_order_side': 'sell',
|
|
'order_filled_timestamp': 1517265300000, 'ft_is_entry': False}
|
|
], [
|
|
{'amount': 0.0097064, 'safe_price': 0.10302485, 'ft_order_side': 'buy',
|
|
'order_filled_timestamp': 1517283000000, 'ft_is_entry': True},
|
|
{'amount': 0.0097064, 'safe_price': 0.10354126528822055, 'ft_order_side': 'sell',
|
|
'order_filled_timestamp': 1517285400000, 'ft_is_entry': False}
|
|
]
|
|
]
|
|
})
|
|
pd.testing.assert_frame_equal(results, expected)
|
|
assert 'orders' in results.columns
|
|
data_pair = processed[pair]
|
|
for _, t in results.iterrows():
|
|
assert len(t['orders']) == 2
|
|
ln = data_pair.loc[data_pair["date"] == t["open_date"]]
|
|
# Check open trade rate aligns to open rate
|
|
assert not ln.empty
|
|
assert round(ln.iloc[0]["open"], 6) == round(t["open_rate"], 6)
|
|
# check close trade rate aligns to close rate or is between high and low
|
|
ln1 = data_pair.loc[data_pair["date"] == t["close_date"]]
|
|
assert (round(ln1.iloc[0]["open"], 6) == round(t["close_rate"], 6) or
|
|
round(ln1.iloc[0]["low"], 6) < round(
|
|
t["close_rate"], 6) < round(ln1.iloc[0]["high"], 6))
|
|
|
|
|
|
@pytest.mark.parametrize('use_detail', [True, False])
|
|
def test_backtest_one_detail(default_conf_usdt, fee, mocker, testdatadir, use_detail) -> None:
|
|
default_conf_usdt['use_exit_signal'] = False
|
|
mocker.patch(f'{EXMS}.get_fee', fee)
|
|
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
|
|
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf'))
|
|
if use_detail:
|
|
default_conf_usdt['timeframe_detail'] = '1m'
|
|
patch_exchange(mocker)
|
|
|
|
def advise_entry(df, *args, **kwargs):
|
|
# Mock function to force several entries
|
|
df.loc[(df['rsi'] < 40), 'enter_long'] = 1
|
|
return df
|
|
|
|
def custom_entry_price(proposed_rate, **kwargs):
|
|
return proposed_rate * 0.997
|
|
|
|
default_conf_usdt['max_open_trades'] = 10
|
|
|
|
backtesting = Backtesting(default_conf_usdt)
|
|
backtesting._set_strategy(backtesting.strategylist[0])
|
|
backtesting.strategy.populate_entry_trend = advise_entry
|
|
backtesting.strategy.custom_entry_price = custom_entry_price
|
|
pair = 'XRP/ETH'
|
|
# Pick a timerange adapted to the pair we use to test
|
|
timerange = TimeRange.parse_timerange('20191010-20191013')
|
|
data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=[pair],
|
|
timerange=timerange)
|
|
if use_detail:
|
|
data_1m = history.load_data(datadir=testdatadir, timeframe='1m', pairs=[pair],
|
|
timerange=timerange)
|
|
backtesting.detail_data = data_1m
|
|
processed = backtesting.strategy.advise_all_indicators(data)
|
|
min_date, max_date = get_timerange(processed)
|
|
|
|
result = backtesting.backtest(
|
|
processed=deepcopy(processed),
|
|
start_date=min_date,
|
|
end_date=max_date,
|
|
)
|
|
results = result['results']
|
|
assert not results.empty
|
|
# Timeout settings from default_conf = entry: 10, exit: 30
|
|
assert len(results) == (2 if use_detail else 3)
|
|
|
|
assert 'orders' in results.columns
|
|
data_pair = processed[pair]
|
|
|
|
data_1m_pair = data_1m[pair] if use_detail else pd.DataFrame()
|
|
late_entry = 0
|
|
for _, t in results.iterrows():
|
|
assert len(t['orders']) == 2
|
|
|
|
entryo = t['orders'][0]
|
|
entry_ts = datetime.fromtimestamp(entryo['order_filled_timestamp'] // 1000, tz=timezone.utc)
|
|
if entry_ts > t['open_date']:
|
|
late_entry += 1
|
|
|
|
# Get "entry fill" candle
|
|
ln = (data_1m_pair.loc[data_1m_pair["date"] == entry_ts]
|
|
if use_detail else data_pair.loc[data_pair["date"] == entry_ts])
|
|
# Check open trade rate aligns to open rate
|
|
assert not ln.empty
|
|
|
|
# assert round(ln.iloc[0]["open"], 6) == round(t["open_rate"], 6)
|
|
assert round(ln.iloc[0]["low"], 6) <= round(
|
|
t["open_rate"], 6) <= round(ln.iloc[0]["high"], 6)
|
|
# check close trade rate aligns to close rate or is between high and low
|
|
ln1 = data_pair.loc[data_pair["date"] == t["close_date"]]
|
|
if use_detail:
|
|
ln1_1m = data_1m_pair.loc[data_1m_pair["date"] == t["close_date"]]
|
|
assert not ln1.empty or not ln1_1m.empty
|
|
else:
|
|
assert not ln1.empty
|
|
ln2 = ln1_1m if ln1.empty else ln1
|
|
|
|
assert (round(ln2.iloc[0]["low"], 6) <= round(
|
|
t["close_rate"], 6) <= round(ln2.iloc[0]["high"], 6))
|
|
|
|
assert late_entry > 0
|
|
|
|
|
|
@pytest.mark.parametrize('use_detail', [True, False])
|
|
def test_backtest_one_detail_futures(
|
|
default_conf_usdt, fee, mocker, testdatadir, use_detail) -> None:
|
|
default_conf_usdt['use_exit_signal'] = False
|
|
default_conf_usdt['trading_mode'] = 'futures'
|
|
default_conf_usdt['margin_mode'] = 'isolated'
|
|
default_conf_usdt['candle_type_def'] = CandleType.FUTURES
|
|
|
|
mocker.patch(f'{EXMS}.get_fee', fee)
|
|
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
|
|
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf'))
|
|
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
|
|
PropertyMock(return_value=['XRP/USDT:USDT']))
|
|
mocker.patch(f"{EXMS}.get_maintenance_ratio_and_amt",
|
|
return_value=(0.01, 0.01))
|
|
default_conf_usdt['timeframe'] = '1h'
|
|
if use_detail:
|
|
default_conf_usdt['timeframe_detail'] = '5m'
|
|
patch_exchange(mocker)
|
|
|
|
def advise_entry(df, *args, **kwargs):
|
|
# Mock function to force several entries
|
|
df.loc[(df['rsi'] < 40), 'enter_long'] = 1
|
|
return df
|
|
|
|
def custom_entry_price(proposed_rate, **kwargs):
|
|
return proposed_rate * 0.997
|
|
|
|
default_conf_usdt['max_open_trades'] = 10
|
|
|
|
backtesting = Backtesting(default_conf_usdt)
|
|
backtesting._set_strategy(backtesting.strategylist[0])
|
|
backtesting.strategy.populate_entry_trend = advise_entry
|
|
backtesting.strategy.custom_entry_price = custom_entry_price
|
|
pair = 'XRP/USDT:USDT'
|
|
# Pick a timerange adapted to the pair we use to test
|
|
timerange = TimeRange.parse_timerange('20211117-20211119')
|
|
data = history.load_data(datadir=Path(testdatadir), timeframe='1h', pairs=[pair],
|
|
timerange=timerange, candle_type=CandleType.FUTURES)
|
|
backtesting.load_bt_data_detail()
|
|
processed = backtesting.strategy.advise_all_indicators(data)
|
|
min_date, max_date = get_timerange(processed)
|
|
|
|
result = backtesting.backtest(
|
|
processed=deepcopy(processed),
|
|
start_date=min_date,
|
|
end_date=max_date,
|
|
)
|
|
results = result['results']
|
|
assert not results.empty
|
|
# Timeout settings from default_conf = entry: 10, exit: 30
|
|
assert len(results) == (5 if use_detail else 2)
|
|
|
|
assert 'orders' in results.columns
|
|
data_pair = processed[pair]
|
|
|
|
data_1m_pair = backtesting.detail_data[pair] if use_detail else pd.DataFrame()
|
|
late_entry = 0
|
|
for _, t in results.iterrows():
|
|
assert len(t['orders']) == 2
|
|
|
|
entryo = t['orders'][0]
|
|
entry_ts = datetime.fromtimestamp(entryo['order_filled_timestamp'] // 1000, tz=timezone.utc)
|
|
if entry_ts > t['open_date']:
|
|
late_entry += 1
|
|
|
|
# Get "entry fill" candle
|
|
ln = (data_1m_pair.loc[data_1m_pair["date"] == entry_ts]
|
|
if use_detail else data_pair.loc[data_pair["date"] == entry_ts])
|
|
# Check open trade rate aligns to open rate
|
|
assert not ln.empty
|
|
|
|
assert round(ln.iloc[0]["low"], 6) <= round(
|
|
t["open_rate"], 6) <= round(ln.iloc[0]["high"], 6)
|
|
# check close trade rate aligns to close rate or is between high and low
|
|
ln1 = data_pair.loc[data_pair["date"] == t["close_date"]]
|
|
if use_detail:
|
|
ln1_1m = data_1m_pair.loc[data_1m_pair["date"] == t["close_date"]]
|
|
assert not ln1.empty or not ln1_1m.empty
|
|
else:
|
|
assert not ln1.empty
|
|
ln2 = ln1_1m if ln1.empty else ln1
|
|
|
|
assert (round(ln2.iloc[0]["low"], 6) <= round(
|
|
t["close_rate"], 6) <= round(ln2.iloc[0]["high"], 6))
|
|
assert -0.0181 < Trade.trades[1].funding_fees < -0.01
|
|
# assert late_entry > 0
|
|
|
|
|
|
@pytest.mark.parametrize('use_detail', [True, False])
|
|
def test_backtest_one_detail_futures_funding_fees(
|
|
default_conf_usdt, fee, mocker, testdatadir, use_detail) -> None:
|
|
default_conf_usdt['use_exit_signal'] = False
|
|
default_conf_usdt['trading_mode'] = 'futures'
|
|
default_conf_usdt['margin_mode'] = 'isolated'
|
|
default_conf_usdt['candle_type_def'] = CandleType.FUTURES
|
|
default_conf_usdt['minimal_roi'] = {'0': 1}
|
|
default_conf_usdt['dry_run_wallet'] = 100000
|
|
|
|
mocker.patch(f'{EXMS}.get_fee', fee)
|
|
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
|
|
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf'))
|
|
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
|
|
PropertyMock(return_value=['XRP/USDT:USDT']))
|
|
mocker.patch(f"{EXMS}.get_maintenance_ratio_and_amt",
|
|
return_value=(0.01, 0.01))
|
|
default_conf_usdt['timeframe'] = '1h'
|
|
if use_detail:
|
|
default_conf_usdt['timeframe_detail'] = '5m'
|
|
patch_exchange(mocker)
|
|
|
|
def advise_entry(df, *args, **kwargs):
|
|
# Mock function to force several entries
|
|
df.loc[:, 'enter_long'] = 1
|
|
return df
|
|
|
|
def adjust_trade_position(trade, current_time, **kwargs):
|
|
if current_time > datetime(2021, 11, 18, 2, 0, 0, tzinfo=timezone.utc):
|
|
return None
|
|
return default_conf_usdt['stake_amount']
|
|
|
|
default_conf_usdt['max_open_trades'] = 1
|
|
|
|
backtesting = Backtesting(default_conf_usdt)
|
|
backtesting._set_strategy(backtesting.strategylist[0])
|
|
backtesting.strategy.populate_entry_trend = advise_entry
|
|
backtesting.strategy.adjust_trade_position = adjust_trade_position
|
|
backtesting.strategy.leverage = lambda **kwargs: 1
|
|
backtesting.strategy.position_adjustment_enable = True
|
|
pair = 'XRP/USDT:USDT'
|
|
# Pick a timerange adapted to the pair we use to test
|
|
timerange = TimeRange.parse_timerange('20211117-20211119')
|
|
data = history.load_data(datadir=Path(testdatadir), timeframe='1h', pairs=[pair],
|
|
timerange=timerange, candle_type=CandleType.FUTURES)
|
|
backtesting.load_bt_data_detail()
|
|
processed = backtesting.strategy.advise_all_indicators(data)
|
|
min_date, max_date = get_timerange(processed)
|
|
|
|
result = backtesting.backtest(
|
|
processed=deepcopy(processed),
|
|
start_date=min_date,
|
|
end_date=max_date,
|
|
)
|
|
results = result['results']
|
|
assert not results.empty
|
|
# Only one result - as we're not selling.
|
|
assert len(results) == 1
|
|
|
|
assert 'orders' in results.columns
|
|
|
|
for t in Trade.trades:
|
|
# At least 4 adjustment orders
|
|
assert t.nr_of_successful_entries >= 6
|
|
# Funding fees will vary depending on the number of adjustment orders
|
|
# That number is a lot higher with detail data.
|
|
assert -20 < t.funding_fees < -0.1
|
|
|
|
|
|
def test_backtest_timedout_entry_orders(default_conf, fee, mocker, testdatadir) -> None:
|
|
# This strategy intentionally places unfillable orders.
|
|
default_conf['strategy'] = 'StrategyTestV3CustomEntryPrice'
|
|
default_conf['startup_candle_count'] = 0
|
|
# Cancel unfilled order after 4 minutes on 5m timeframe.
|
|
default_conf["unfilledtimeout"] = {"entry": 4}
|
|
mocker.patch(f'{EXMS}.get_fee', fee)
|
|
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
|
|
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf'))
|
|
patch_exchange(mocker)
|
|
default_conf['max_open_trades'] = 1
|
|
backtesting = Backtesting(default_conf)
|
|
backtesting._set_strategy(backtesting.strategylist[0])
|
|
# Testing dataframe contains 11 candles. Expecting 10 timed out orders.
|
|
timerange = TimeRange('date', 'date', 1517227800, 1517231100)
|
|
data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'],
|
|
timerange=timerange)
|
|
min_date, max_date = get_timerange(data)
|
|
|
|
result = backtesting.backtest(
|
|
processed=deepcopy(data),
|
|
start_date=min_date,
|
|
end_date=max_date,
|
|
)
|
|
|
|
assert result['timedout_entry_orders'] == 10
|
|
|
|
|
|
def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None:
|
|
default_conf['use_exit_signal'] = False
|
|
default_conf['max_open_trades'] = 1
|
|
mocker.patch(f'{EXMS}.get_fee', fee)
|
|
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
|
|
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf'))
|
|
patch_exchange(mocker)
|
|
backtesting = Backtesting(default_conf)
|
|
backtesting._set_strategy(backtesting.strategylist[0])
|
|
|
|
# Run a backtesting for an exiting 1min timeframe
|
|
timerange = TimeRange.parse_timerange('1510688220-1510700340')
|
|
data = history.load_data(datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC'],
|
|
timerange=timerange)
|
|
processed = backtesting.strategy.advise_all_indicators(data)
|
|
min_date, max_date = get_timerange(processed)
|
|
results = backtesting.backtest(
|
|
processed=processed,
|
|
start_date=min_date,
|
|
end_date=max_date,
|
|
)
|
|
assert not results['results'].empty
|
|
assert len(results['results']) == 1
|
|
|
|
|
|
def test_backtest_trim_no_data_left(default_conf, fee, mocker, testdatadir) -> None:
|
|
default_conf['use_exit_signal'] = False
|
|
default_conf['max_open_trades'] = 10
|
|
|
|
mocker.patch(f'{EXMS}.get_fee', fee)
|
|
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
|
|
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf'))
|
|
patch_exchange(mocker)
|
|
backtesting = Backtesting(default_conf)
|
|
backtesting._set_strategy(backtesting.strategylist[0])
|
|
timerange = TimeRange('date', None, 1517227800, 0)
|
|
backtesting.required_startup = 100
|
|
backtesting.timerange = timerange
|
|
data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'],
|
|
timerange=timerange)
|
|
df = data['UNITTEST/BTC']
|
|
df['date'] = df.loc[:, 'date'] - timedelta(days=1)
|
|
# Trimming 100 candles, so after 2nd trimming, no candle is left.
|
|
df = df.iloc[:100]
|
|
data['XRP/USDT'] = df
|
|
processed = backtesting.strategy.advise_all_indicators(data)
|
|
min_date, max_date = get_timerange(processed)
|
|
|
|
backtesting.backtest(
|
|
processed=deepcopy(processed),
|
|
start_date=min_date,
|
|
end_date=max_date,
|
|
)
|
|
|
|
|
|
def test_processed(default_conf, mocker, testdatadir) -> None:
|
|
patch_exchange(mocker)
|
|
backtesting = Backtesting(default_conf)
|
|
backtesting._set_strategy(backtesting.strategylist[0])
|
|
|
|
dict_of_tickerrows = load_data_test('raise', testdatadir)
|
|
dataframes = backtesting.strategy.advise_all_indicators(dict_of_tickerrows)
|
|
dataframe = dataframes['UNITTEST/BTC']
|
|
cols = dataframe.columns
|
|
# assert the dataframe got some of the indicator columns
|
|
for col in ['close', 'high', 'low', 'open', 'date',
|
|
'ema10', 'rsi', 'fastd', 'plus_di']:
|
|
assert col in cols
|
|
|
|
|
|
def test_backtest_dataprovider_analyzed_df(default_conf, fee, mocker, testdatadir) -> None:
|
|
default_conf['use_exit_signal'] = False
|
|
default_conf['max_open_trades'] = 10
|
|
mocker.patch(f'{EXMS}.get_fee', fee)
|
|
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
|
|
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=100000)
|
|
patch_exchange(mocker)
|
|
backtesting = Backtesting(default_conf)
|
|
backtesting._set_strategy(backtesting.strategylist[0])
|
|
timerange = TimeRange('date', None, 1517227800, 0)
|
|
data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'],
|
|
timerange=timerange)
|
|
processed = backtesting.strategy.advise_all_indicators(data)
|
|
min_date, max_date = get_timerange(processed)
|
|
|
|
global count
|
|
count = 0
|
|
|
|
def tmp_confirm_entry(pair, current_time, **kwargs):
|
|
dp = backtesting.strategy.dp
|
|
df, _ = dp.get_analyzed_dataframe(pair, backtesting.strategy.timeframe)
|
|
current_candle = df.iloc[-1].squeeze()
|
|
assert current_candle['enter_long'] == 1
|
|
|
|
candle_date = timeframe_to_next_date(backtesting.strategy.timeframe, current_candle['date'])
|
|
assert candle_date == current_time
|
|
# These asserts don't properly raise as they are nested,
|
|
# therefore we increment count and assert for that.
|
|
global count
|
|
count = count + 1
|
|
|
|
backtesting.strategy.confirm_trade_entry = tmp_confirm_entry
|
|
backtesting.backtest(
|
|
processed=deepcopy(processed),
|
|
start_date=min_date,
|
|
end_date=max_date,
|
|
)
|
|
assert count == 5
|
|
|
|
|
|
def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatadir) -> None:
|
|
# While this test IS a copy of test_backtest_pricecontours, it's needed to ensure
|
|
# results do not carry-over to the next run, which is not given by using parametrize.
|
|
patch_exchange(mocker)
|
|
default_conf['protections'] = [
|
|
{
|
|
"method": "CooldownPeriod",
|
|
"stop_duration": 3,
|
|
}]
|
|
|
|
default_conf['enable_protections'] = True
|
|
default_conf['timeframe'] = '1m'
|
|
default_conf['max_open_trades'] = 1
|
|
mocker.patch(f'{EXMS}.get_fee', fee)
|
|
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
|
|
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf'))
|
|
tests = [
|
|
['sine', 9],
|
|
['raise', 10],
|
|
['lower', 0],
|
|
['sine', 9],
|
|
['raise', 10],
|
|
]
|
|
backtesting = Backtesting(default_conf)
|
|
backtesting._set_strategy(backtesting.strategylist[0])
|
|
|
|
# While entry-signals are unrealistic, running backtesting
|
|
# over and over again should not cause different results
|
|
for [contour, numres] in tests:
|
|
# Debug output for random test failure
|
|
print(f"{contour}, {numres}")
|
|
data = load_data_test(contour, testdatadir)
|
|
processed = backtesting.strategy.advise_all_indicators(data)
|
|
min_date, max_date = get_timerange(processed)
|
|
assert isinstance(processed, dict)
|
|
results = backtesting.backtest(
|
|
processed=processed,
|
|
start_date=min_date,
|
|
end_date=max_date,
|
|
)
|
|
assert len(results['results']) == numres
|
|
|
|
|
|
@pytest.mark.parametrize('protections,contour,expected', [
|
|
(None, 'sine', 35),
|
|
(None, 'raise', 19),
|
|
(None, 'lower', 0),
|
|
(None, 'sine', 35),
|
|
(None, 'raise', 19),
|
|
([{"method": "CooldownPeriod", "stop_duration": 3}], 'sine', 9),
|
|
([{"method": "CooldownPeriod", "stop_duration": 3}], 'raise', 10),
|
|
([{"method": "CooldownPeriod", "stop_duration": 3}], 'lower', 0),
|
|
([{"method": "CooldownPeriod", "stop_duration": 3}], 'sine', 9),
|
|
([{"method": "CooldownPeriod", "stop_duration": 3}], 'raise', 10),
|
|
])
|
|
def test_backtest_pricecontours(default_conf, fee, mocker, testdatadir,
|
|
protections, contour, expected) -> None:
|
|
if protections:
|
|
default_conf['protections'] = protections
|
|
default_conf['enable_protections'] = True
|
|
|
|
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
|
|
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf'))
|
|
mocker.patch(f'{EXMS}.get_fee', fee)
|
|
# While entry-signals are unrealistic, running backtesting
|
|
# over and over again should not cause different results
|
|
|
|
patch_exchange(mocker)
|
|
default_conf['timeframe'] = '1m'
|
|
backtesting = Backtesting(default_conf)
|
|
backtesting._set_strategy(backtesting.strategylist[0])
|
|
|
|
data = load_data_test(contour, testdatadir)
|
|
processed = backtesting.strategy.advise_all_indicators(data)
|
|
min_date, max_date = get_timerange(processed)
|
|
assert isinstance(processed, dict)
|
|
backtesting.strategy.max_open_trades = 1
|
|
backtesting.config.update({'max_open_trades': 1})
|
|
results = backtesting.backtest(
|
|
processed=processed,
|
|
start_date=min_date,
|
|
end_date=max_date,
|
|
)
|
|
assert len(results['results']) == expected
|
|
|
|
|
|
def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir):
|
|
# Override the default buy trend function in our StrategyTest
|
|
def fun(dataframe=None, pair=None):
|
|
buy_value = 1
|
|
sell_value = 1
|
|
return _trend(dataframe, buy_value, sell_value)
|
|
default_conf['max_open_trades'] = 10
|
|
backtest_conf = _make_backtest_conf(mocker, conf=default_conf, datadir=testdatadir)
|
|
backtesting = Backtesting(default_conf)
|
|
backtesting._set_strategy(backtesting.strategylist[0])
|
|
backtesting.strategy.advise_entry = fun # Override
|
|
backtesting.strategy.advise_exit = fun # Override
|
|
result = backtesting.backtest(**backtest_conf)
|
|
assert result['results'].empty
|
|
|
|
|
|
def test_backtest_only_sell(mocker, default_conf, testdatadir):
|
|
# Override the default buy trend function in our StrategyTest
|
|
def fun(dataframe=None, pair=None):
|
|
buy_value = 0
|
|
sell_value = 1
|
|
return _trend(dataframe, buy_value, sell_value)
|
|
|
|
default_conf['max_open_trades'] = 10
|
|
backtest_conf = _make_backtest_conf(mocker, conf=default_conf, datadir=testdatadir)
|
|
backtesting = Backtesting(default_conf)
|
|
backtesting._set_strategy(backtesting.strategylist[0])
|
|
backtesting.strategy.advise_entry = fun # Override
|
|
backtesting.strategy.advise_exit = fun # Override
|
|
result = backtesting.backtest(**backtest_conf)
|
|
assert result['results'].empty
|
|
|
|
|
|
def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir):
|
|
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
|
|
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf'))
|
|
mocker.patch(f'{EXMS}.get_fee', fee)
|
|
default_conf['max_open_trades'] = 10
|
|
backtest_conf = _make_backtest_conf(mocker, conf=default_conf,
|
|
pair='UNITTEST/BTC', datadir=testdatadir)
|
|
default_conf['timeframe'] = '1m'
|
|
backtesting = Backtesting(default_conf)
|
|
backtesting.required_startup = 0
|
|
backtesting._set_strategy(backtesting.strategylist[0])
|
|
backtesting.strategy.advise_entry = _trend_alternate # Override
|
|
backtesting.strategy.advise_exit = _trend_alternate # Override
|
|
result = backtesting.backtest(**backtest_conf)
|
|
# 200 candles in backtest data
|
|
# won't buy on first (shifted by 1)
|
|
# 100 buys signals
|
|
results = result['results']
|
|
assert len(results) == 100
|
|
# Cached data should be 200
|
|
analyzed_df = backtesting.dataprovider.get_analyzed_dataframe('UNITTEST/BTC', '1m')[0]
|
|
assert len(analyzed_df) == 200
|
|
# Expect last candle to be 1 below end date (as the last candle is assumed as "incomplete"
|
|
# during backtesting)
|
|
expected_last_candle_date = backtest_conf['end_date'] - timedelta(minutes=1)
|
|
assert analyzed_df.iloc[-1]['date'].to_pydatetime() == expected_last_candle_date
|
|
|
|
# One trade was force-closed at the end
|
|
assert len(results.loc[results['is_open']]) == 0
|
|
|
|
|
|
@pytest.mark.parametrize("pair", ['ADA/BTC', 'LTC/BTC'])
|
|
@pytest.mark.parametrize("tres", [0, 20, 30])
|
|
def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir):
|
|
|
|
def _trend_alternate_hold(dataframe=None, metadata=None):
|
|
"""
|
|
Buy every xth candle - sell every other xth -2 (hold on to pairs a bit)
|
|
"""
|
|
if metadata['pair'] in ('ETH/BTC', 'LTC/BTC'):
|
|
multi = 20
|
|
else:
|
|
multi = 18
|
|
dataframe['enter_long'] = np.where(dataframe.index % multi == 0, 1, 0)
|
|
dataframe['exit_long'] = np.where((dataframe.index + multi - 2) % multi == 0, 1, 0)
|
|
dataframe['enter_short'] = 0
|
|
dataframe['exit_short'] = 0
|
|
return dataframe
|
|
|
|
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
|
|
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf'))
|
|
mocker.patch(f'{EXMS}.get_fee', fee)
|
|
patch_exchange(mocker)
|
|
|
|
pairs = ['ADA/BTC', 'DASH/BTC', 'ETH/BTC', 'LTC/BTC', 'NXT/BTC']
|
|
data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=pairs)
|
|
# Only use 500 lines to increase performance
|
|
data = trim_dictlist(data, -500)
|
|
|
|
# Remove data for one pair from the beginning of the data
|
|
if tres > 0:
|
|
data[pair] = data[pair][tres:].reset_index()
|
|
default_conf['timeframe'] = '5m'
|
|
default_conf['max_open_trades'] = 3
|
|
|
|
backtesting = Backtesting(default_conf)
|
|
backtesting._set_strategy(backtesting.strategylist[0])
|
|
backtesting.strategy.advise_entry = _trend_alternate_hold # Override
|
|
backtesting.strategy.advise_exit = _trend_alternate_hold # Override
|
|
|
|
processed = backtesting.strategy.advise_all_indicators(data)
|
|
min_date, max_date = get_timerange(processed)
|
|
|
|
backtest_conf = {
|
|
'processed': deepcopy(processed),
|
|
'start_date': min_date,
|
|
'end_date': max_date,
|
|
}
|
|
|
|
results = backtesting.backtest(**backtest_conf)
|
|
|
|
# Make sure we have parallel trades
|
|
assert len(evaluate_result_multi(results['results'], '5m', 2)) > 0
|
|
# make sure we don't have trades with more than configured max_open_trades
|
|
assert len(evaluate_result_multi(results['results'], '5m', 3)) == 0
|
|
|
|
# Cached data correctly removed amounts
|
|
offset = 1 if tres == 0 else 0
|
|
removed_candles = len(data[pair]) - offset - backtesting.strategy.startup_candle_count
|
|
assert len(backtesting.dataprovider.get_analyzed_dataframe(pair, '5m')[0]) == removed_candles
|
|
assert len(
|
|
backtesting.dataprovider.get_analyzed_dataframe('NXT/BTC', '5m')[0]
|
|
) == len(data['NXT/BTC']) - 1 - backtesting.strategy.startup_candle_count
|
|
|
|
backtesting.strategy.max_open_trades = 1
|
|
backtesting.config.update({'max_open_trades': 1})
|
|
backtest_conf = {
|
|
'processed': deepcopy(processed),
|
|
'start_date': min_date,
|
|
'end_date': max_date,
|
|
}
|
|
results = backtesting.backtest(**backtest_conf)
|
|
assert len(evaluate_result_multi(results['results'], '5m', 1)) == 0
|
|
|
|
|
|
def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir):
|
|
|
|
patch_exchange(mocker)
|
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest')
|
|
mocker.patch('freqtrade.optimize.backtesting.generate_backtest_stats')
|
|
mocker.patch('freqtrade.optimize.backtesting.show_backtest_results')
|
|
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
|
|
PropertyMock(return_value=['UNITTEST/BTC']))
|
|
patched_configuration_load_config_file(mocker, default_conf)
|
|
|
|
args = [
|
|
'backtesting',
|
|
'--config', 'config.json',
|
|
'--strategy', CURRENT_TEST_STRATEGY,
|
|
'--datadir', str(testdatadir),
|
|
'--timeframe', '1m',
|
|
'--timerange', '1510694220-1510700340',
|
|
'--enable-position-stacking',
|
|
'--disable-max-market-positions'
|
|
]
|
|
args = get_args(args)
|
|
start_backtesting(args)
|
|
# check the logs, that will contain the backtest result
|
|
exists = [
|
|
'Parameter -i/--timeframe detected ... Using timeframe: 1m ...',
|
|
'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
|
|
'Parameter --timerange detected: 1510694220-1510700340 ...',
|
|
f'Using data directory: {testdatadir} ...',
|
|
'Loading data from 2017-11-14 20:57:00 '
|
|
'up to 2017-11-14 22:59:00 (0 days).',
|
|
'Backtesting with data from 2017-11-14 21:17:00 '
|
|
'up to 2017-11-14 22:59:00 (0 days).',
|
|
'Parameter --enable-position-stacking detected ...'
|
|
]
|
|
|
|
for line in exists:
|
|
assert log_has(line, caplog)
|
|
|
|
|
|
@pytest.mark.filterwarnings("ignore:deprecated")
|
|
def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
|
|
|
|
default_conf.update({
|
|
"use_exit_signal": True,
|
|
"exit_profit_only": False,
|
|
"exit_profit_offset": 0.0,
|
|
"ignore_roi_if_entry_signal": False,
|
|
})
|
|
patch_exchange(mocker)
|
|
backtestmock = MagicMock(return_value={
|
|
'results': pd.DataFrame(columns=BT_DATA_COLUMNS),
|
|
'config': default_conf,
|
|
'locks': [],
|
|
'rejected_signals': 20,
|
|
'timedout_entry_orders': 0,
|
|
'timedout_exit_orders': 0,
|
|
'canceled_trade_entries': 0,
|
|
'canceled_entry_orders': 0,
|
|
'replaced_entry_orders': 0,
|
|
'final_balance': 1000,
|
|
})
|
|
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
|
|
PropertyMock(return_value=['UNITTEST/BTC']))
|
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
|
|
text_table_mock = MagicMock()
|
|
sell_reason_mock = MagicMock()
|
|
strattable_mock = MagicMock()
|
|
strat_summary = MagicMock()
|
|
|
|
mocker.patch.multiple('freqtrade.optimize.optimize_reports',
|
|
text_table_bt_results=text_table_mock,
|
|
text_table_strategy=strattable_mock,
|
|
generate_pair_metrics=MagicMock(),
|
|
generate_exit_reason_stats=sell_reason_mock,
|
|
generate_strategy_comparison=strat_summary,
|
|
generate_daily_stats=MagicMock(),
|
|
)
|
|
patched_configuration_load_config_file(mocker, default_conf)
|
|
|
|
args = [
|
|
'backtesting',
|
|
'--config', 'config.json',
|
|
'--datadir', str(testdatadir),
|
|
'--strategy-path', str(Path(__file__).parents[1] / 'strategy/strats'),
|
|
'--timeframe', '1m',
|
|
'--timerange', '1510694220-1510700340',
|
|
'--enable-position-stacking',
|
|
'--disable-max-market-positions',
|
|
'--strategy-list',
|
|
CURRENT_TEST_STRATEGY,
|
|
'StrategyTestV2',
|
|
]
|
|
args = get_args(args)
|
|
start_backtesting(args)
|
|
# 2 backtests, 4 tables
|
|
assert backtestmock.call_count == 2
|
|
assert text_table_mock.call_count == 4
|
|
assert strattable_mock.call_count == 1
|
|
assert sell_reason_mock.call_count == 2
|
|
assert strat_summary.call_count == 1
|
|
|
|
# check the logs, that will contain the backtest result
|
|
exists = [
|
|
'Parameter -i/--timeframe detected ... Using timeframe: 1m ...',
|
|
'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
|
|
'Parameter --timerange detected: 1510694220-1510700340 ...',
|
|
f'Using data directory: {testdatadir} ...',
|
|
'Loading data from 2017-11-14 20:57:00 '
|
|
'up to 2017-11-14 22:59:00 (0 days).',
|
|
'Backtesting with data from 2017-11-14 21:17:00 '
|
|
'up to 2017-11-14 22:59:00 (0 days).',
|
|
'Parameter --enable-position-stacking detected ...',
|
|
f'Running backtesting for Strategy {CURRENT_TEST_STRATEGY}',
|
|
'Running backtesting for Strategy StrategyTestV2',
|
|
]
|
|
|
|
for line in exists:
|
|
assert log_has(line, caplog)
|
|
|
|
|
|
def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdatadir, capsys):
|
|
default_conf.update({
|
|
"use_exit_signal": True,
|
|
"exit_profit_only": False,
|
|
"exit_profit_offset": 0.0,
|
|
"ignore_roi_if_entry_signal": False,
|
|
})
|
|
patch_exchange(mocker)
|
|
result1 = pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC'],
|
|
'profit_ratio': [0.0, 0.0],
|
|
'profit_abs': [0.0, 0.0],
|
|
'open_date': pd.to_datetime(['2018-01-29 18:40:00',
|
|
'2018-01-30 03:30:00', ], utc=True
|
|
),
|
|
'close_date': pd.to_datetime(['2018-01-29 20:45:00',
|
|
'2018-01-30 05:35:00', ], utc=True),
|
|
'trade_duration': [235, 40],
|
|
'is_open': [False, False],
|
|
'stake_amount': [0.01, 0.01],
|
|
'open_rate': [0.104445, 0.10302485],
|
|
'close_rate': [0.104969, 0.103541],
|
|
"is_short": [False, False],
|
|
|
|
'exit_reason': [ExitType.ROI, ExitType.ROI]
|
|
})
|
|
result2 = pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC', 'ETH/BTC'],
|
|
'profit_ratio': [0.03, 0.01, 0.1],
|
|
'profit_abs': [0.01, 0.02, 0.2],
|
|
'open_date': pd.to_datetime(['2018-01-29 18:40:00',
|
|
'2018-01-30 03:30:00',
|
|
'2018-01-30 05:30:00'], utc=True
|
|
),
|
|
'close_date': pd.to_datetime(['2018-01-29 20:45:00',
|
|
'2018-01-30 05:35:00',
|
|
'2018-01-30 08:30:00'], utc=True),
|
|
'trade_duration': [47, 40, 20],
|
|
'is_open': [False, False, False],
|
|
'stake_amount': [0.01, 0.01, 0.01],
|
|
'open_rate': [0.104445, 0.10302485, 0.122541],
|
|
'close_rate': [0.104969, 0.103541, 0.123541],
|
|
"is_short": [False, False, False],
|
|
'exit_reason': [ExitType.ROI, ExitType.ROI, ExitType.STOP_LOSS]
|
|
})
|
|
backtestmock = MagicMock(side_effect=[
|
|
{
|
|
'results': result1,
|
|
'config': default_conf,
|
|
'locks': [],
|
|
'rejected_signals': 20,
|
|
'timedout_entry_orders': 0,
|
|
'timedout_exit_orders': 0,
|
|
'canceled_trade_entries': 0,
|
|
'canceled_entry_orders': 0,
|
|
'replaced_entry_orders': 0,
|
|
'final_balance': 1000,
|
|
},
|
|
{
|
|
'results': result2,
|
|
'config': default_conf,
|
|
'locks': [],
|
|
'rejected_signals': 20,
|
|
'timedout_entry_orders': 0,
|
|
'timedout_exit_orders': 0,
|
|
'canceled_trade_entries': 0,
|
|
'canceled_entry_orders': 0,
|
|
'replaced_entry_orders': 0,
|
|
'final_balance': 1000,
|
|
}
|
|
])
|
|
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
|
|
PropertyMock(return_value=['UNITTEST/BTC']))
|
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
|
|
|
|
patched_configuration_load_config_file(mocker, default_conf)
|
|
|
|
args = [
|
|
'backtesting',
|
|
'--config', 'config.json',
|
|
'--datadir', str(testdatadir),
|
|
'--strategy-path', str(Path(__file__).parents[1] / 'strategy/strats'),
|
|
'--timeframe', '1m',
|
|
'--timerange', '1510694220-1510700340',
|
|
'--enable-position-stacking',
|
|
'--disable-max-market-positions',
|
|
'--breakdown', 'day',
|
|
'--strategy-list',
|
|
CURRENT_TEST_STRATEGY,
|
|
'StrategyTestV2',
|
|
]
|
|
args = get_args(args)
|
|
start_backtesting(args)
|
|
|
|
# check the logs, that will contain the backtest result
|
|
exists = [
|
|
'Parameter -i/--timeframe detected ... Using timeframe: 1m ...',
|
|
'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
|
|
'Parameter --timerange detected: 1510694220-1510700340 ...',
|
|
f'Using data directory: {testdatadir} ...',
|
|
'Loading data from 2017-11-14 20:57:00 '
|
|
'up to 2017-11-14 22:59:00 (0 days).',
|
|
'Backtesting with data from 2017-11-14 21:17:00 '
|
|
'up to 2017-11-14 22:59:00 (0 days).',
|
|
'Parameter --enable-position-stacking detected ...',
|
|
f'Running backtesting for Strategy {CURRENT_TEST_STRATEGY}',
|
|
'Running backtesting for Strategy StrategyTestV2',
|
|
]
|
|
|
|
for line in exists:
|
|
assert log_has(line, caplog)
|
|
|
|
captured = capsys.readouterr()
|
|
assert 'BACKTESTING REPORT' in captured.out
|
|
assert 'EXIT REASON STATS' in captured.out
|
|
assert 'DAY BREAKDOWN' in captured.out
|
|
assert 'LEFT OPEN TRADES REPORT' in captured.out
|
|
assert '2017-11-14 21:17:00 -> 2017-11-14 22:59:00 | Max open trades : 1' in captured.out
|
|
assert 'STRATEGY SUMMARY' in captured.out
|
|
|
|
|
|
@pytest.mark.filterwarnings("ignore:deprecated")
|
|
def test_backtest_start_futures_noliq(default_conf_usdt, mocker,
|
|
caplog, testdatadir, capsys):
|
|
# Tests detail-data loading
|
|
default_conf_usdt.update({
|
|
"trading_mode": "futures",
|
|
"margin_mode": "isolated",
|
|
"use_exit_signal": True,
|
|
"exit_profit_only": False,
|
|
"exit_profit_offset": 0.0,
|
|
"ignore_roi_if_entry_signal": False,
|
|
"strategy": CURRENT_TEST_STRATEGY,
|
|
})
|
|
patch_exchange(mocker)
|
|
|
|
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
|
|
PropertyMock(return_value=['HULUMULU/USDT', 'XRP/USDT:USDT']))
|
|
# mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
|
|
|
|
patched_configuration_load_config_file(mocker, default_conf_usdt)
|
|
|
|
args = [
|
|
'backtesting',
|
|
'--config', 'config.json',
|
|
'--datadir', str(testdatadir),
|
|
'--strategy-path', str(Path(__file__).parents[1] / 'strategy/strats'),
|
|
'--timeframe', '1h',
|
|
]
|
|
args = get_args(args)
|
|
with pytest.raises(OperationalException, match=r"Pairs .* got no leverage tiers available\."):
|
|
start_backtesting(args)
|
|
|
|
|
|
@pytest.mark.filterwarnings("ignore:deprecated")
|
|
def test_backtest_start_nomock_futures(default_conf_usdt, mocker,
|
|
caplog, testdatadir, capsys):
|
|
# Tests detail-data loading
|
|
default_conf_usdt.update({
|
|
"trading_mode": "futures",
|
|
"margin_mode": "isolated",
|
|
"use_exit_signal": True,
|
|
"exit_profit_only": False,
|
|
"exit_profit_offset": 0.0,
|
|
"ignore_roi_if_entry_signal": False,
|
|
"strategy": CURRENT_TEST_STRATEGY,
|
|
})
|
|
patch_exchange(mocker)
|
|
result1 = pd.DataFrame({'pair': ['XRP/USDT:USDT', 'XRP/USDT:USDT'],
|
|
'profit_ratio': [0.0, 0.0],
|
|
'profit_abs': [0.0, 0.0],
|
|
'open_date': pd.to_datetime(['2021-11-18 18:00:00',
|
|
'2021-11-18 03:00:00', ], utc=True
|
|
),
|
|
'close_date': pd.to_datetime(['2021-11-18 20:00:00',
|
|
'2021-11-18 05:00:00', ], utc=True),
|
|
'trade_duration': [235, 40],
|
|
'is_open': [False, False],
|
|
'is_short': [False, False],
|
|
'stake_amount': [0.01, 0.01],
|
|
'open_rate': [0.104445, 0.10302485],
|
|
'close_rate': [0.104969, 0.103541],
|
|
'exit_reason': [ExitType.ROI, ExitType.ROI]
|
|
})
|
|
result2 = pd.DataFrame({'pair': ['XRP/USDT:USDT', 'XRP/USDT:USDT', 'XRP/USDT:USDT'],
|
|
'profit_ratio': [0.03, 0.01, 0.1],
|
|
'profit_abs': [0.01, 0.02, 0.2],
|
|
'open_date': pd.to_datetime(['2021-11-19 18:00:00',
|
|
'2021-11-19 03:00:00',
|
|
'2021-11-19 05:00:00'], utc=True
|
|
),
|
|
'close_date': pd.to_datetime(['2021-11-19 20:00:00',
|
|
'2021-11-19 05:00:00',
|
|
'2021-11-19 08:00:00'], utc=True),
|
|
'trade_duration': [47, 40, 20],
|
|
'is_open': [False, False, False],
|
|
'is_short': [False, False, False],
|
|
'stake_amount': [0.01, 0.01, 0.01],
|
|
'open_rate': [0.104445, 0.10302485, 0.122541],
|
|
'close_rate': [0.104969, 0.103541, 0.123541],
|
|
'exit_reason': [ExitType.ROI, ExitType.ROI, ExitType.STOP_LOSS]
|
|
})
|
|
backtestmock = MagicMock(side_effect=[
|
|
{
|
|
'results': result1,
|
|
'config': default_conf_usdt,
|
|
'locks': [],
|
|
'rejected_signals': 20,
|
|
'timedout_entry_orders': 0,
|
|
'timedout_exit_orders': 0,
|
|
'canceled_trade_entries': 0,
|
|
'canceled_entry_orders': 0,
|
|
'replaced_entry_orders': 0,
|
|
'final_balance': 1000,
|
|
},
|
|
{
|
|
'results': result2,
|
|
'config': default_conf_usdt,
|
|
'locks': [],
|
|
'rejected_signals': 20,
|
|
'timedout_entry_orders': 0,
|
|
'timedout_exit_orders': 0,
|
|
'canceled_trade_entries': 0,
|
|
'canceled_entry_orders': 0,
|
|
'replaced_entry_orders': 0,
|
|
'final_balance': 1000,
|
|
}
|
|
])
|
|
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
|
|
PropertyMock(return_value=['XRP/USDT:USDT']))
|
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
|
|
|
|
patched_configuration_load_config_file(mocker, default_conf_usdt)
|
|
|
|
args = [
|
|
'backtesting',
|
|
'--config', 'config.json',
|
|
'--datadir', str(testdatadir),
|
|
'--strategy-path', str(Path(__file__).parents[1] / 'strategy/strats'),
|
|
'--timeframe', '1h',
|
|
]
|
|
args = get_args(args)
|
|
start_backtesting(args)
|
|
|
|
# check the logs, that will contain the backtest result
|
|
exists = [
|
|
'Parameter -i/--timeframe detected ... Using timeframe: 1h ...',
|
|
f'Using data directory: {testdatadir} ...',
|
|
'Loading data from 2021-11-17 01:00:00 '
|
|
'up to 2021-11-21 04:00:00 (4 days).',
|
|
'Backtesting with data from 2021-11-17 21:00:00 '
|
|
'up to 2021-11-21 04:00:00 (3 days).',
|
|
'XRP/USDT:USDT, funding_rate, 8h, data starts at 2021-11-18 00:00:00',
|
|
'XRP/USDT:USDT, mark, 8h, data starts at 2021-11-18 00:00:00',
|
|
f'Running backtesting for Strategy {CURRENT_TEST_STRATEGY}',
|
|
]
|
|
|
|
for line in exists:
|
|
assert log_has(line, caplog)
|
|
|
|
captured = capsys.readouterr()
|
|
assert 'BACKTESTING REPORT' in captured.out
|
|
assert 'EXIT REASON STATS' in captured.out
|
|
assert 'LEFT OPEN TRADES REPORT' in captured.out
|
|
|
|
|
|
@pytest.mark.filterwarnings("ignore:deprecated")
|
|
def test_backtest_start_multi_strat_nomock_detail(default_conf, mocker,
|
|
caplog, testdatadir, capsys):
|
|
# Tests detail-data loading
|
|
default_conf.update({
|
|
"use_exit_signal": True,
|
|
"exit_profit_only": False,
|
|
"exit_profit_offset": 0.0,
|
|
"ignore_roi_if_entry_signal": False,
|
|
})
|
|
patch_exchange(mocker)
|
|
result1 = pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC'],
|
|
'profit_ratio': [0.0, 0.0],
|
|
'profit_abs': [0.0, 0.0],
|
|
'open_date': pd.to_datetime(['2018-01-29 18:40:00',
|
|
'2018-01-30 03:30:00', ], utc=True
|
|
),
|
|
'close_date': pd.to_datetime(['2018-01-29 20:45:00',
|
|
'2018-01-30 05:35:00', ], utc=True),
|
|
'trade_duration': [235, 40],
|
|
'is_open': [False, False],
|
|
'is_short': [False, False],
|
|
'stake_amount': [0.01, 0.01],
|
|
'open_rate': [0.104445, 0.10302485],
|
|
'close_rate': [0.104969, 0.103541],
|
|
'exit_reason': [ExitType.ROI, ExitType.ROI]
|
|
})
|
|
result2 = pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC', 'ETH/BTC'],
|
|
'profit_ratio': [0.03, 0.01, 0.1],
|
|
'profit_abs': [0.01, 0.02, 0.2],
|
|
'open_date': pd.to_datetime(['2018-01-29 18:40:00',
|
|
'2018-01-30 03:30:00',
|
|
'2018-01-30 05:30:00'], utc=True
|
|
),
|
|
'close_date': pd.to_datetime(['2018-01-29 20:45:00',
|
|
'2018-01-30 05:35:00',
|
|
'2018-01-30 08:30:00'], utc=True),
|
|
'trade_duration': [47, 40, 20],
|
|
'is_open': [False, False, False],
|
|
'is_short': [False, False, False],
|
|
'stake_amount': [0.01, 0.01, 0.01],
|
|
'open_rate': [0.104445, 0.10302485, 0.122541],
|
|
'close_rate': [0.104969, 0.103541, 0.123541],
|
|
'exit_reason': [ExitType.ROI, ExitType.ROI, ExitType.STOP_LOSS]
|
|
})
|
|
backtestmock = MagicMock(side_effect=[
|
|
{
|
|
'results': result1,
|
|
'config': default_conf,
|
|
'locks': [],
|
|
'rejected_signals': 20,
|
|
'timedout_entry_orders': 0,
|
|
'timedout_exit_orders': 0,
|
|
'canceled_trade_entries': 0,
|
|
'canceled_entry_orders': 0,
|
|
'replaced_entry_orders': 0,
|
|
'final_balance': 1000,
|
|
},
|
|
{
|
|
'results': result2,
|
|
'config': default_conf,
|
|
'locks': [],
|
|
'rejected_signals': 20,
|
|
'timedout_entry_orders': 0,
|
|
'timedout_exit_orders': 0,
|
|
'canceled_trade_entries': 0,
|
|
'canceled_entry_orders': 0,
|
|
'replaced_entry_orders': 0,
|
|
'final_balance': 1000,
|
|
}
|
|
])
|
|
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
|
|
PropertyMock(return_value=['XRP/ETH']))
|
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
|
|
|
|
patched_configuration_load_config_file(mocker, default_conf)
|
|
|
|
args = [
|
|
'backtesting',
|
|
'--config', 'config.json',
|
|
'--datadir', str(testdatadir),
|
|
'--strategy-path', str(Path(__file__).parents[1] / 'strategy/strats'),
|
|
'--timeframe', '5m',
|
|
'--timeframe-detail', '1m',
|
|
'--strategy-list',
|
|
CURRENT_TEST_STRATEGY
|
|
]
|
|
args = get_args(args)
|
|
start_backtesting(args)
|
|
|
|
# check the logs, that will contain the backtest result
|
|
exists = [
|
|
'Parameter -i/--timeframe detected ... Using timeframe: 5m ...',
|
|
'Parameter --timeframe-detail detected, using 1m for intra-candle backtesting ...',
|
|
f'Using data directory: {testdatadir} ...',
|
|
'Loading data from 2019-10-11 00:00:00 '
|
|
'up to 2019-10-13 11:15:00 (2 days).',
|
|
'Backtesting with data from 2019-10-11 01:40:00 '
|
|
'up to 2019-10-13 11:15:00 (2 days).',
|
|
f'Running backtesting for Strategy {CURRENT_TEST_STRATEGY}',
|
|
]
|
|
|
|
for line in exists:
|
|
assert log_has(line, caplog)
|
|
|
|
captured = capsys.readouterr()
|
|
assert 'BACKTESTING REPORT' in captured.out
|
|
assert 'EXIT REASON STATS' in captured.out
|
|
assert 'LEFT OPEN TRADES REPORT' in captured.out
|
|
|
|
|
|
@pytest.mark.filterwarnings("ignore:deprecated")
|
|
@pytest.mark.parametrize('run_id', ['2', 'changed'])
|
|
@pytest.mark.parametrize('start_delta', [{'days': 0}, {'days': 1}, {'weeks': 1}, {'weeks': 4}])
|
|
@pytest.mark.parametrize('cache', constants.BACKTEST_CACHE_AGE)
|
|
def test_backtest_start_multi_strat_caching(default_conf, mocker, caplog, testdatadir, run_id,
|
|
start_delta, cache):
|
|
default_conf.update({
|
|
"use_exit_signal": True,
|
|
"exit_profit_only": False,
|
|
"exit_profit_offset": 0.0,
|
|
"ignore_roi_if_entry_signal": False,
|
|
})
|
|
patch_exchange(mocker)
|
|
backtestmock = MagicMock(return_value={
|
|
'results': pd.DataFrame(columns=BT_DATA_COLUMNS),
|
|
'config': default_conf,
|
|
'locks': [],
|
|
'rejected_signals': 20,
|
|
'timedout_entry_orders': 0,
|
|
'timedout_exit_orders': 0,
|
|
'canceled_trade_entries': 0,
|
|
'canceled_entry_orders': 0,
|
|
'replaced_entry_orders': 0,
|
|
'final_balance': 1000,
|
|
})
|
|
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
|
|
PropertyMock(return_value=['UNITTEST/BTC']))
|
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
|
|
mocker.patch('freqtrade.optimize.backtesting.show_backtest_results', MagicMock())
|
|
|
|
now = min_backtest_date = datetime.now(tz=timezone.utc)
|
|
start_time = now - timedelta(**start_delta) + timedelta(hours=1)
|
|
if cache == 'none':
|
|
min_backtest_date = now + timedelta(days=1)
|
|
elif cache == 'day':
|
|
min_backtest_date = now - timedelta(days=1)
|
|
elif cache == 'week':
|
|
min_backtest_date = now - timedelta(weeks=1)
|
|
elif cache == 'month':
|
|
min_backtest_date = now - timedelta(weeks=4)
|
|
load_backtest_metadata = MagicMock(return_value={
|
|
'StrategyTestV2': {'run_id': '1', 'backtest_start_time': now.timestamp()},
|
|
'StrategyTestV3': {'run_id': run_id, 'backtest_start_time': start_time.timestamp()}
|
|
})
|
|
load_backtest_stats = MagicMock(side_effect=[
|
|
{
|
|
'metadata': {'StrategyTestV2': {'run_id': '1'}},
|
|
'strategy': {'StrategyTestV2': {}},
|
|
'strategy_comparison': [{'key': 'StrategyTestV2'}]
|
|
},
|
|
{
|
|
'metadata': {'StrategyTestV3': {'run_id': '2'}},
|
|
'strategy': {'StrategyTestV3': {}},
|
|
'strategy_comparison': [{'key': 'StrategyTestV3'}]
|
|
}
|
|
])
|
|
mocker.patch('pathlib.Path.glob', return_value=[
|
|
Path(datetime.strftime(datetime.now(), 'backtest-result-%Y-%m-%d_%H-%M-%S.json'))])
|
|
mocker.patch.multiple('freqtrade.data.btanalysis',
|
|
load_backtest_metadata=load_backtest_metadata,
|
|
load_backtest_stats=load_backtest_stats)
|
|
mocker.patch('freqtrade.optimize.backtesting.get_strategy_run_id', side_effect=['1', '2', '2'])
|
|
|
|
patched_configuration_load_config_file(mocker, default_conf)
|
|
|
|
args = [
|
|
'backtesting',
|
|
'--config', 'config.json',
|
|
'--datadir', str(testdatadir),
|
|
'--strategy-path', str(Path(__file__).parents[1] / 'strategy/strats'),
|
|
'--timeframe', '1m',
|
|
'--timerange', '1510694220-1510700340',
|
|
'--enable-position-stacking',
|
|
'--disable-max-market-positions',
|
|
'--cache', cache,
|
|
'--strategy-list',
|
|
'StrategyTestV2',
|
|
'StrategyTestV3',
|
|
]
|
|
args = get_args(args)
|
|
start_backtesting(args)
|
|
|
|
# check the logs, that will contain the backtest result
|
|
exists = [
|
|
'Parameter -i/--timeframe detected ... Using timeframe: 1m ...',
|
|
'Parameter --timerange detected: 1510694220-1510700340 ...',
|
|
f'Using data directory: {testdatadir} ...',
|
|
'Loading data from 2017-11-14 20:57:00 '
|
|
'up to 2017-11-14 22:59:00 (0 days).',
|
|
'Parameter --enable-position-stacking detected ...',
|
|
]
|
|
|
|
for line in exists:
|
|
assert log_has(line, caplog)
|
|
|
|
if cache == 'none':
|
|
assert backtestmock.call_count == 2
|
|
exists = [
|
|
'Running backtesting for Strategy StrategyTestV2',
|
|
'Running backtesting for Strategy StrategyTestV3',
|
|
'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
|
|
'Backtesting with data from 2017-11-14 21:17:00 up to 2017-11-14 22:59:00 (0 days).',
|
|
]
|
|
elif run_id == '2' and min_backtest_date < start_time:
|
|
assert backtestmock.call_count == 0
|
|
exists = [
|
|
'Reusing result of previous backtest for StrategyTestV2',
|
|
'Reusing result of previous backtest for StrategyTestV3',
|
|
]
|
|
else:
|
|
exists = [
|
|
'Reusing result of previous backtest for StrategyTestV2',
|
|
'Running backtesting for Strategy StrategyTestV3',
|
|
'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
|
|
'Backtesting with data from 2017-11-14 21:17:00 up to 2017-11-14 22:59:00 (0 days).',
|
|
]
|
|
assert backtestmock.call_count == 1
|
|
|
|
for line in exists:
|
|
assert log_has(line, caplog)
|
|
|
|
|
|
def test_get_strategy_run_id(default_conf_usdt):
|
|
default_conf_usdt.update({
|
|
'strategy': 'StrategyTestV2',
|
|
'max_open_trades': float('inf')
|
|
})
|
|
strategy = StrategyResolver.load_strategy(default_conf_usdt)
|
|
x = get_strategy_run_id(strategy)
|
|
assert isinstance(x, str)
|