Fixed max_open_trades update from hyperopt

Fixed max_open_trades update from hyperopt + removed max_open_trades as a param to backtesting + refactoring
This commit is contained in:
Antonio Della Fortuna 2023-01-08 12:39:39 +01:00
parent 8c3ac56bc5
commit 464cb4761c
7 changed files with 132 additions and 52 deletions

View File

@ -252,7 +252,7 @@ AVAILABLE_CLI_OPTIONS = {
'--spaces',
help='Specify which parameters to hyperopt. Space-separated list.',
choices=['all', 'buy', 'sell', 'roi', 'stoploss',
'trailing', 'protection', 'default', 'trades'],
'trailing', 'protection', 'trades', 'default'],
nargs='+',
default='default',
),

View File

@ -920,8 +920,9 @@ class Backtesting:
trade.close(exit_row[OPEN_IDX], show_msg=False)
LocalTrade.close_bt_trade(trade)
def trade_slot_available(self, max_open_trades: int, open_trade_count: int) -> bool:
def trade_slot_available(self, open_trade_count: int) -> bool:
# Always allow trades when max_open_trades is enabled.
max_open_trades = self.config['max_open_trades']
if max_open_trades <= 0 or open_trade_count < max_open_trades:
return True
# Rejected trade
@ -1051,7 +1052,6 @@ class Backtesting:
def backtest_loop(
self, row: Tuple, pair: str, current_time: datetime, end_date: datetime,
max_open_trades: int,
open_trade_count_start: int, is_first: bool = True) -> int:
"""
NOTE: This method is used by Hyperopt at each iteration. Please keep it optimized.
@ -1075,7 +1075,7 @@ class Backtesting:
if (
(self._position_stacking or len(LocalTrade.bt_trades_open_pp[pair]) == 0)
and is_first
and self.trade_slot_available(max_open_trades, open_trade_count_start)
and self.trade_slot_available(open_trade_count_start)
and current_time != end_date
and trade_dir is not None
and not PairLocks.is_pair_locked(pair, row[DATE_IDX], trade_dir)
@ -1122,8 +1122,7 @@ class Backtesting:
return open_trade_count_start
def backtest(self, processed: Dict,
start_date: datetime, end_date: datetime,
max_open_trades: int = 0) -> Dict[str, Any]:
start_date: datetime, end_date: datetime) -> Dict[str, Any]:
"""
Implement backtesting functionality
@ -1135,7 +1134,6 @@ class Backtesting:
optimize memory usage!
:param start_date: backtesting timerange start datetime
:param end_date: backtesting timerange end datetime
:param max_open_trades: maximum number of concurrent trades, <= 0 means unlimited
:return: DataFrame with trades (results of backtesting)
"""
self.prepare_backtest(self.enable_protections)
@ -1176,7 +1174,7 @@ class Backtesting:
if len(detail_data) == 0:
# Fall back to "regular" data if no detail data was found for this candle
open_trade_count_start = self.backtest_loop(
row, pair, current_time, end_date, max_open_trades,
row, pair, current_time, end_date,
open_trade_count_start)
continue
detail_data.loc[:, 'enter_long'] = row[LONG_IDX]
@ -1189,13 +1187,13 @@ class Backtesting:
current_time_det = current_time
for det_row in detail_data[HEADERS].values.tolist():
open_trade_count_start = self.backtest_loop(
det_row, pair, current_time_det, end_date, max_open_trades,
det_row, pair, current_time_det, end_date,
open_trade_count_start, is_first)
current_time_det += timedelta(minutes=self.timeframe_detail_min)
is_first = False
else:
open_trade_count_start = self.backtest_loop(
row, pair, current_time, end_date, max_open_trades, open_trade_count_start)
row, pair, current_time, end_date, open_trade_count_start)
# Move time one configured time_interval ahead.
self.progress.increment()
@ -1227,14 +1225,11 @@ class Backtesting:
self._set_strategy(strat)
# Use max_open_trades in backtesting, except --disable-max-market-positions is set
if self.config.get('use_max_market_positions', True):
# Must come from strategy config, as the strategy may modify this setting.
max_open_trades = self.strategy.config['max_open_trades'] \
if self.strategy.config['max_open_trades'] != float('inf') else -1
else:
if not self.config.get('use_max_market_positions', True):
logger.info(
'Ignoring max_open_trades (--disable-max-market-positions was used) ...')
max_open_trades = 0
self.strategy.max_open_trades = -1
self.config.update({'max_open_trades': float('inf')})
# need to reprocess data every time to populate signals
preprocessed = self.strategy.advise_all_indicators(data)
@ -1257,7 +1252,6 @@ class Backtesting:
processed=preprocessed,
start_date=min_date,
end_date=max_date,
max_open_trades=max_open_trades,
)
backtest_end_time = datetime.now(timezone.utc)
results.update({

View File

@ -118,14 +118,10 @@ class Hyperopt:
self.current_best_epoch: Optional[Dict[str, Any]] = None
# Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set
if self.config.get('use_max_market_positions', True):
self.max_open_trades = self.config['max_open_trades'] \
if self.config['max_open_trades'] != float('inf') else -1
else:
if not self.config.get('use_max_market_positions', True):
logger.debug('Ignoring max_open_trades (--disable-max-market-positions was used) ...')
self.max_open_trades = 0
print("Strategy max open trades", self.max_open_trades)
self.backtesting.strategy.max_open_trades = -1
config.update({'max_open_trades': float('inf')})
if HyperoptTools.has_space(self.config, 'sell'):
# Make sure use_exit_signal is enabled
@ -214,7 +210,8 @@ class Hyperopt:
if HyperoptTools.has_space(self.config, 'trailing'):
result['trailing'] = self.custom_hyperopt.generate_trailing_params(params)
if HyperoptTools.has_space(self.config, 'trades'):
result['max_open_trades'] = {p.name: params.get(p.name) for p in self.trades_space}
result['max_open_trades'] = {
'max_open_trades': self.backtesting.strategy.max_open_trades}
return result
@ -342,7 +339,21 @@ class Hyperopt:
d['trailing_only_offset_is_reached']
if HyperoptTools.has_space(self.config, 'trades'):
self.max_open_trades = params_dict['max_open_trades']
if self.config["stake_amount"] == "unlimited" and \
(params_dict['max_open_trades'] == -1 or params_dict['max_open_trades'] == 0):
# Ignore unlimited max open trades if stake amount is unlimited
params_dict.update({'max_open_trades': self.config['max_open_trades']})
updated_config_max_open_trades = int(params_dict['max_open_trades']) \
if (params_dict['max_open_trades'] != -1
and params_dict['max_open_trades'] != 0) else float('inf')
updated_strategy_max_open_trades = int(updated_config_max_open_trades) \
if updated_config_max_open_trades != float('inf') else -1
self.config.update({'max_open_trades': updated_config_max_open_trades})
self.backtesting.strategy.max_open_trades = updated_strategy_max_open_trades
with self.data_pickle_file.open('rb') as f:
processed = load(f, mmap_mode='r')
@ -353,8 +364,7 @@ class Hyperopt:
bt_results = self.backtesting.backtest(
processed=processed,
start_date=self.min_date,
end_date=self.max_date,
max_open_trades=self.max_open_trades,
end_date=self.max_date
)
backtest_end_time = datetime.now(timezone.utc)
bt_results.update({

View File

@ -919,6 +919,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data: BTContainer)
default_conf["trailing_stop_positive"] = data.trailing_stop_positive
default_conf["trailing_stop_positive_offset"] = data.trailing_stop_positive_offset
default_conf["use_exit_signal"] = data.use_exit_signal
default_conf["max_open_trades"] = 10
mocker.patch("freqtrade.exchange.Exchange.get_fee", return_value=0.0)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
@ -951,7 +952,6 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data: BTContainer)
processed=data_processed,
start_date=min_date,
end_date=max_date,
max_open_trades=10,
)
results = result['results']

View File

@ -96,7 +96,6 @@ def _make_backtest_conf(mocker, datadir, conf=None, pair='UNITTEST/BTC'):
'processed': processed,
'start_date': min_date,
'end_date': max_date,
'max_open_trades': 10,
}
@ -684,6 +683,8 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> 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('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
@ -701,7 +702,6 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
processed=deepcopy(processed),
start_date=min_date,
end_date=max_date,
max_open_trades=10,
)
results = result['results']
assert not results.empty
@ -785,6 +785,8 @@ def test_backtest_one_detail(default_conf_usdt, fee, mocker, testdatadir, use_de
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
@ -805,7 +807,6 @@ def test_backtest_one_detail(default_conf_usdt, fee, mocker, testdatadir, use_de
processed=deepcopy(processed),
start_date=min_date,
end_date=max_date,
max_open_trades=10,
)
results = result['results']
assert not results.empty
@ -859,6 +860,7 @@ def test_backtest_timedout_entry_orders(default_conf, fee, mocker, testdatadir)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
mocker.patch("freqtrade.exchange.Exchange.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.
@ -871,7 +873,6 @@ def test_backtest_timedout_entry_orders(default_conf, fee, mocker, testdatadir)
processed=deepcopy(data),
start_date=min_date,
end_date=max_date,
max_open_trades=1,
)
assert result['timedout_entry_orders'] == 10
@ -879,6 +880,7 @@ def test_backtest_timedout_entry_orders(default_conf, fee, mocker, testdatadir)
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('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
@ -896,7 +898,6 @@ def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None
processed=processed,
start_date=min_date,
end_date=max_date,
max_open_trades=1,
)
assert not results['results'].empty
assert len(results['results']) == 1
@ -904,6 +905,8 @@ def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None
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('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
@ -927,7 +930,6 @@ def test_backtest_trim_no_data_left(default_conf, fee, mocker, testdatadir) -> N
processed=deepcopy(processed),
start_date=min_date,
end_date=max_date,
max_open_trades=10,
)
@ -948,6 +950,7 @@ def test_processed(default_conf, mocker, testdatadir) -> None:
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('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=100000)
@ -981,7 +984,6 @@ def test_backtest_dataprovider_analyzed_df(default_conf, fee, mocker, testdatadi
processed=deepcopy(processed),
start_date=min_date,
end_date=max_date,
max_open_trades=10,
)
assert count == 5
@ -998,6 +1000,7 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad
default_conf['enable_protections'] = True
default_conf['timeframe'] = '1m'
default_conf['max_open_trades'] = 1
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
@ -1024,7 +1027,6 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad
processed=processed,
start_date=min_date,
end_date=max_date,
max_open_trades=1,
)
assert len(results['results']) == numres
@ -1062,11 +1064,12 @@ def test_backtest_pricecontours(default_conf, fee, mocker, 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,
max_open_trades=1,
)
assert len(results['results']) == expected
@ -1077,7 +1080,7 @@ def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir):
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])
@ -1094,6 +1097,7 @@ def test_backtest_only_sell(mocker, default_conf, testdatadir):
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])
@ -1107,6 +1111,7 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir):
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
mocker.patch('freqtrade.exchange.Exchange.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'
@ -1165,6 +1170,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
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])
@ -1173,11 +1179,11 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
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,
'max_open_trades': 3,
}
results = backtesting.backtest(**backtest_conf)
@ -1195,11 +1201,12 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
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,
'max_open_trades': 1,
}
results = backtesting.backtest(**backtest_conf)
assert len(evaluate_result_multi(results['results'], '5m', 1)) == 0

View File

@ -17,6 +17,7 @@ from tests.conftest import patch_exchange
def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) -> None:
default_conf['use_exit_signal'] = False
default_conf['max_open_trades'] = 10
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch('freqtrade.optimize.backtesting.amount_to_contract_precision',
lambda x, *args, **kwargs: round(x, 8))
@ -41,7 +42,6 @@ def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) ->
processed=deepcopy(processed),
start_date=min_date,
end_date=max_date,
max_open_trades=10,
)
results = result['results']
assert not results.empty

View File

@ -1,5 +1,6 @@
# pragma pylint: disable=missing-docstring,W0212,C0103
from datetime import datetime, timedelta
from functools import wraps
from pathlib import Path
from unittest.mock import ANY, MagicMock, PropertyMock
@ -336,8 +337,7 @@ def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None:
assert dumper2.call_count == 1
assert hasattr(hyperopt.backtesting.strategy, "advise_exit")
assert hasattr(hyperopt.backtesting.strategy, "advise_entry")
assert hasattr(hyperopt, "max_open_trades")
assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades']
assert hyperopt.backtesting.strategy.max_open_trades == hyperopt_conf['max_open_trades']
assert hasattr(hyperopt.backtesting, "_position_stacking")
@ -708,8 +708,7 @@ def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> Non
assert hasattr(hyperopt.backtesting.strategy, "advise_exit")
assert hasattr(hyperopt.backtesting.strategy, "advise_entry")
assert hasattr(hyperopt, "max_open_trades")
assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades']
assert hyperopt.backtesting.strategy.max_open_trades == hyperopt_conf['max_open_trades']
assert hasattr(hyperopt.backtesting, "_position_stacking")
@ -782,8 +781,7 @@ def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None:
assert dumper2.call_count == 1
assert hasattr(hyperopt.backtesting.strategy, "advise_exit")
assert hasattr(hyperopt.backtesting.strategy, "advise_entry")
assert hasattr(hyperopt, "max_open_trades")
assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades']
assert hyperopt.backtesting.strategy.max_open_trades == hyperopt_conf['max_open_trades']
assert hasattr(hyperopt.backtesting, "_position_stacking")
@ -825,8 +823,7 @@ def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None:
assert dumper2.call_count == 1
assert hasattr(hyperopt.backtesting.strategy, "advise_exit")
assert hasattr(hyperopt.backtesting.strategy, "advise_entry")
assert hasattr(hyperopt, "max_open_trades")
assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades']
assert hyperopt.backtesting.strategy.max_open_trades == hyperopt_conf['max_open_trades']
assert hasattr(hyperopt.backtesting, "_position_stacking")
@ -880,7 +877,7 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None:
assert hyperopt.backtesting.strategy.buy_rsi.value == 35
assert hyperopt.backtesting.strategy.sell_rsi.value == 74
assert hyperopt.backtesting.strategy.protection_cooldown_lookback.value == 30
assert hyperopt.max_open_trades == 1
assert hyperopt.backtesting.strategy.max_open_trades == 1
buy_rsi_range = hyperopt.backtesting.strategy.buy_rsi.range
assert isinstance(buy_rsi_range, range)
# Range from 0 - 50 (inclusive)
@ -891,7 +888,7 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None:
assert hyperopt.backtesting.strategy.protection_cooldown_lookback.value != 30
assert hyperopt.backtesting.strategy.buy_rsi.value != 35
assert hyperopt.backtesting.strategy.sell_rsi.value != 74
assert hyperopt.max_open_trades != 1
assert hyperopt.backtesting.strategy.max_open_trades != 1
hyperopt.custom_hyperopt.generate_estimator = lambda *args, **kwargs: 'ET1'
with pytest.raises(OperationalException, match="Estimator ET1 not supported."):
@ -992,3 +989,75 @@ def test_SKDecimal():
assert space.transform([2.0]) == [200]
assert space.transform([1.0]) == [100]
assert space.transform([1.5, 1.6]) == [150, 160]
def test_stake_amount_unlimited_max_open_trades(mocker, hyperopt_conf, tmpdir, fee) -> None:
# This test is to ensure that unlimited max_open_trades are ignored for the backtesting
# if we have an unlimited stake amount
patch_exchange(mocker)
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
(Path(tmpdir) / 'hyperopt_results').mkdir(parents=True)
hyperopt_conf.update({
'strategy': 'HyperoptableStrategy',
'user_data_dir': Path(tmpdir),
'hyperopt_random_state': 42,
'spaces': ['trades'],
'stake_amount': 'unlimited'
})
hyperopt = Hyperopt(hyperopt_conf)
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._get_params_dict',
return_value={
'max_open_trades': -1
})
assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto)
assert hyperopt.backtesting.strategy.max_open_trades == 1
hyperopt.start()
assert hyperopt.backtesting.strategy.max_open_trades == 1
def test_max_open_trades_consistency(mocker, hyperopt_conf, tmpdir, fee) -> None:
# This test is to ensure that max_open_trades is the same across all functions needing it
# after it has been changed from the hyperopt
patch_exchange(mocker)
mocker.patch('freqtrade.exchange.Exchange.get_fee', return_value=0)
(Path(tmpdir) / 'hyperopt_results').mkdir(parents=True)
hyperopt_conf.update({
'strategy': 'HyperoptableStrategy',
'user_data_dir': Path(tmpdir),
'hyperopt_random_state': 42,
'spaces': ['trades'],
'stake_amount': 'unlimited',
'dry_run_wallet': 8,
'available_capital': 8,
'dry_run': True,
'epochs': 1
})
hyperopt = Hyperopt(hyperopt_conf)
assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto)
first_time_evaluated = False
def stake_amount_interceptor(func):
@wraps(func)
def wrapper(*args, **kwargs):
nonlocal first_time_evaluated
stake_amount = func(*args, **kwargs)
if first_time_evaluated is False:
assert stake_amount == 1
first_time_evaluated = True
return stake_amount
return wrapper
hyperopt.backtesting.wallets._calculate_unlimited_stake_amount = stake_amount_interceptor(
hyperopt.backtesting.wallets._calculate_unlimited_stake_amount)
hyperopt.start()
assert hyperopt.backtesting.strategy.max_open_trades == 8
assert hyperopt.config['max_open_trades'] == 8