mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-11-10 10:21:59 +00:00
Merge pull request #7976 from adarkforce/max-open-trades
Hyperopt Max open trades
This commit is contained in:
commit
4aaa439221
|
@ -75,7 +75,7 @@ This function needs to return a floating point number (`float`). Smaller numbers
|
|||
|
||||
## Overriding pre-defined spaces
|
||||
|
||||
To override a pre-defined space (`roi_space`, `generate_roi_table`, `stoploss_space`, `trailing_space`), define a nested class called Hyperopt and define the required spaces as follows:
|
||||
To override a pre-defined space (`roi_space`, `generate_roi_table`, `stoploss_space`, `trailing_space`, `max_open_trades_space`), define a nested class called Hyperopt and define the required spaces as follows:
|
||||
|
||||
```python
|
||||
from freqtrade.optimize.space import Categorical, Dimension, Integer, SKDecimal
|
||||
|
@ -123,6 +123,12 @@ class MyAwesomeStrategy(IStrategy):
|
|||
|
||||
Categorical([True, False], name='trailing_only_offset_is_reached'),
|
||||
]
|
||||
|
||||
# Define a custom max_open_trades space
|
||||
def max_open_trades_space(self) -> List[Dimension]:
|
||||
return [
|
||||
Integer(-1, 10, name='max_open_trades'),
|
||||
]
|
||||
```
|
||||
|
||||
!!! Note
|
||||
|
|
|
@ -134,7 +134,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||
|
||||
| Parameter | Description |
|
||||
|------------|-------------|
|
||||
| `max_open_trades` | **Required.** Number of open trades your bot is allowed to have. Only one open trade per pair is possible, so the length of your pairlist is another limitation that can apply. If -1 then it is ignored (i.e. potentially unlimited open trades, limited by the pairlist). [More information below](#configuring-amount-per-trade).<br> **Datatype:** Positive integer or -1.
|
||||
| `max_open_trades` | **Required.** Number of open trades your bot is allowed to have. Only one open trade per pair is possible, so the length of your pairlist is another limitation that can apply. If -1 then it is ignored (i.e. potentially unlimited open trades, limited by the pairlist). [More information below](#configuring-amount-per-trade). [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Positive integer or -1.
|
||||
| `stake_currency` | **Required.** Crypto-currency used for trading. <br> **Datatype:** String
|
||||
| `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#configuring-amount-per-trade). <br> **Datatype:** Positive float or `"unlimited"`.
|
||||
| `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.99` 99%).*<br> **Datatype:** Positive float between `0.1` and `1.0`.
|
||||
|
@ -263,6 +263,7 @@ Values set in the configuration file always overwrite values set in the strategy
|
|||
* `minimal_roi`
|
||||
* `timeframe`
|
||||
* `stoploss`
|
||||
* `max_open_trades`
|
||||
* `trailing_stop`
|
||||
* `trailing_stop_positive`
|
||||
* `trailing_stop_positive_offset`
|
||||
|
|
|
@ -50,7 +50,7 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
|
|||
[--eps] [--dmmp] [--enable-protections]
|
||||
[--dry-run-wallet DRY_RUN_WALLET]
|
||||
[--timeframe-detail TIMEFRAME_DETAIL] [-e INT]
|
||||
[--spaces {all,buy,sell,roi,stoploss,trailing,protection,default} [{all,buy,sell,roi,stoploss,trailing,protection,default} ...]]
|
||||
[--spaces {all,buy,sell,roi,stoploss,trailing,protection,trades,default} [{all,buy,sell,roi,stoploss,trailing,protection,trades,default} ...]]
|
||||
[--print-all] [--no-color] [--print-json] [-j JOBS]
|
||||
[--random-state INT] [--min-trades INT]
|
||||
[--hyperopt-loss NAME] [--disable-param-export]
|
||||
|
@ -96,7 +96,7 @@ optional arguments:
|
|||
Specify detail timeframe for backtesting (`1m`, `5m`,
|
||||
`30m`, `1h`, `1d`).
|
||||
-e INT, --epochs INT Specify number of epochs (default: 100).
|
||||
--spaces {all,buy,sell,roi,stoploss,trailing,protection,default} [{all,buy,sell,roi,stoploss,trailing,protection,default} ...]
|
||||
--spaces {all,buy,sell,roi,stoploss,trailing,protection,trades,default} [{all,buy,sell,roi,stoploss,trailing,protection,trades,default} ...]
|
||||
Specify which parameters to hyperopt. Space-separated
|
||||
list.
|
||||
--print-all Print all results, not only the best ones.
|
||||
|
@ -180,6 +180,7 @@ Rarely you may also need to create a [nested class](advanced-hyperopt.md#overrid
|
|||
* `generate_roi_table` - for custom ROI optimization (if you need the ranges for the values in the ROI table that differ from default or the number of entries (steps) in the ROI table which differs from the default 4 steps)
|
||||
* `stoploss_space` - for custom stoploss optimization (if you need the range for the stoploss parameter in the optimization hyperspace that differs from default)
|
||||
* `trailing_space` - for custom trailing stop optimization (if you need the ranges for the trailing stop parameters in the optimization hyperspace that differ from default)
|
||||
* `max_open_trades_space` - for custom max_open_trades optimization (if you need the ranges for the max_open_trades parameter in the optimization hyperspace that differ from default)
|
||||
|
||||
!!! Tip "Quickly optimize ROI, stoploss and trailing stoploss"
|
||||
You can quickly optimize the spaces `roi`, `stoploss` and `trailing` without changing anything in your strategy.
|
||||
|
@ -643,6 +644,7 @@ Legal values are:
|
|||
* `roi`: just optimize the minimal profit table for your strategy
|
||||
* `stoploss`: search for the best stoploss value
|
||||
* `trailing`: search for the best trailing stop values
|
||||
* `trades`: search for the best max open trades values
|
||||
* `protection`: search for the best protection parameters (read the [protections section](#optimizing-protections) on how to properly define these)
|
||||
* `default`: `all` except `trailing` and `protection`
|
||||
* space-separated list of any of the above values for example `--spaces roi stoploss`
|
||||
|
@ -916,5 +918,5 @@ Once the optimized strategy has been implemented into your strategy, you should
|
|||
To achieve same the results (number of trades, their durations, profit, etc.) as during Hyperopt, please use the same configuration and parameters (timerange, timeframe, ...) used for hyperopt `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting.
|
||||
|
||||
Should results not match, please double-check to make sure you transferred all conditions correctly.
|
||||
Pay special care to the stoploss (and trailing stoploss) parameters, as these are often set in configuration files, which override changes to the strategy.
|
||||
You should also carefully review the log of your backtest to ensure that there were no parameters inadvertently set by the configuration (like `stoploss` or `trailing_stop`).
|
||||
Pay special care to the stoploss, max_open_trades and trailing stoploss parameters, as these are often set in configuration files, which override changes to the strategy.
|
||||
You should also carefully review the log of your backtest to ensure that there were no parameters inadvertently set by the configuration (like `stoploss`, `max_open_trades` or `trailing_stop`).
|
||||
|
|
|
@ -251,7 +251,8 @@ AVAILABLE_CLI_OPTIONS = {
|
|||
"spaces": Arg(
|
||||
'--spaces',
|
||||
help='Specify which parameters to hyperopt. Space-separated list.',
|
||||
choices=['all', 'buy', 'sell', 'roi', 'stoploss', 'trailing', 'protection', 'default'],
|
||||
choices=['all', 'buy', 'sell', 'roi', 'stoploss',
|
||||
'trailing', 'protection', 'trades', 'default'],
|
||||
nargs='+',
|
||||
default='default',
|
||||
),
|
||||
|
|
|
@ -636,7 +636,6 @@ SCHEMA_TRADE_REQUIRED = [
|
|||
|
||||
SCHEMA_BACKTEST_REQUIRED = [
|
||||
'exchange',
|
||||
'max_open_trades',
|
||||
'stake_currency',
|
||||
'stake_amount',
|
||||
'dry_run_wallet',
|
||||
|
@ -646,6 +645,7 @@ SCHEMA_BACKTEST_REQUIRED = [
|
|||
SCHEMA_BACKTEST_REQUIRED_FINAL = SCHEMA_BACKTEST_REQUIRED + [
|
||||
'stoploss',
|
||||
'minimal_roi',
|
||||
'max_open_trades'
|
||||
]
|
||||
|
||||
SCHEMA_MINIMAL_REQUIRED = [
|
||||
|
@ -681,3 +681,4 @@ MakerTaker = Literal['maker', 'taker']
|
|||
BidAsk = Literal['bid', 'ask']
|
||||
|
||||
Config = Dict[str, Any]
|
||||
IntOrInf = float
|
||||
|
|
|
@ -10,7 +10,7 @@ from typing import Any, Dict, List, Optional, Union
|
|||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from freqtrade.constants import LAST_BT_RESULT_FN
|
||||
from freqtrade.constants import LAST_BT_RESULT_FN, IntOrInf
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.misc import json_load
|
||||
from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename
|
||||
|
@ -332,7 +332,7 @@ def analyze_trade_parallelism(results: pd.DataFrame, timeframe: str) -> pd.DataF
|
|||
|
||||
|
||||
def evaluate_result_multi(results: pd.DataFrame, timeframe: str,
|
||||
max_open_trades: int) -> pd.DataFrame:
|
||||
max_open_trades: IntOrInf) -> pd.DataFrame:
|
||||
"""
|
||||
Find overlapping trades by expanding each trade once per period it was open
|
||||
and then counting overlaps
|
||||
|
|
|
@ -15,7 +15,7 @@ from pandas import DataFrame
|
|||
|
||||
from freqtrade import constants
|
||||
from freqtrade.configuration import TimeRange, validate_config_consistency
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, Config, LongShort
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, Config, IntOrInf, LongShort
|
||||
from freqtrade.data import history
|
||||
from freqtrade.data.btanalysis import find_existing_backtest_stats, trade_list_to_dataframe
|
||||
from freqtrade.data.converter import trim_dataframe, trim_dataframes
|
||||
|
@ -922,8 +922,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: IntOrInf = self.config['max_open_trades']
|
||||
if max_open_trades <= 0 or open_trade_count < max_open_trades:
|
||||
return True
|
||||
# Rejected trade
|
||||
|
@ -1053,7 +1054,7 @@ 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, trade_dir: Optional[LongShort],
|
||||
open_trade_count_start: int, trade_dir: Optional[LongShort],
|
||||
is_first: bool = True) -> int:
|
||||
"""
|
||||
NOTE: This method is used by Hyperopt at each iteration. Please keep it optimized.
|
||||
|
@ -1076,7 +1077,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)
|
||||
|
@ -1123,8 +1124,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
|
||||
|
||||
|
@ -1136,7 +1136,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)
|
||||
|
@ -1185,7 +1184,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, trade_dir)
|
||||
continue
|
||||
detail_data.loc[:, 'enter_long'] = row[LONG_IDX]
|
||||
|
@ -1198,13 +1197,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, trade_dir, 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,
|
||||
row, pair, current_time, end_date,
|
||||
open_trade_count_start, trade_dir)
|
||||
|
||||
# Move time one configured time_interval ahead.
|
||||
|
@ -1237,13 +1236,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']
|
||||
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 = float('inf')
|
||||
self.config.update({'max_open_trades': self.strategy.max_open_trades})
|
||||
|
||||
# need to reprocess data every time to populate signals
|
||||
preprocessed = self.strategy.advise_all_indicators(data)
|
||||
|
@ -1266,7 +1263,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({
|
||||
|
|
|
@ -74,6 +74,7 @@ class Hyperopt:
|
|||
self.roi_space: List[Dimension] = []
|
||||
self.stoploss_space: List[Dimension] = []
|
||||
self.trailing_space: List[Dimension] = []
|
||||
self.max_open_trades_space: List[Dimension] = []
|
||||
self.dimensions: List[Dimension] = []
|
||||
|
||||
self.config = config
|
||||
|
@ -117,11 +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']
|
||||
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
|
||||
self.backtesting.strategy.max_open_trades = float('inf')
|
||||
config.update({'max_open_trades': self.backtesting.strategy.max_open_trades})
|
||||
|
||||
if HyperoptTools.has_space(self.config, 'sell'):
|
||||
# Make sure use_exit_signal is enabled
|
||||
|
@ -209,6 +209,10 @@ class Hyperopt:
|
|||
result['stoploss'] = {p.name: params.get(p.name) for p in self.stoploss_space}
|
||||
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'] = {
|
||||
'max_open_trades': self.backtesting.strategy.max_open_trades
|
||||
if self.backtesting.strategy.max_open_trades != float('inf') else -1}
|
||||
|
||||
return result
|
||||
|
||||
|
@ -229,6 +233,8 @@ class Hyperopt:
|
|||
'trailing_stop_positive_offset': strategy.trailing_stop_positive_offset,
|
||||
'trailing_only_offset_is_reached': strategy.trailing_only_offset_is_reached,
|
||||
}
|
||||
if not HyperoptTools.has_space(self.config, 'trades'):
|
||||
result['max_open_trades'] = {'max_open_trades': strategy.max_open_trades}
|
||||
return result
|
||||
|
||||
def print_results(self, results) -> None:
|
||||
|
@ -280,8 +286,13 @@ class Hyperopt:
|
|||
logger.debug("Hyperopt has 'trailing' space")
|
||||
self.trailing_space = self.custom_hyperopt.trailing_space()
|
||||
|
||||
if HyperoptTools.has_space(self.config, 'trades'):
|
||||
logger.debug("Hyperopt has 'trades' space")
|
||||
self.max_open_trades_space = self.custom_hyperopt.max_open_trades_space()
|
||||
|
||||
self.dimensions = (self.buy_space + self.sell_space + self.protection_space
|
||||
+ self.roi_space + self.stoploss_space + self.trailing_space)
|
||||
+ self.roi_space + self.stoploss_space + self.trailing_space
|
||||
+ self.max_open_trades_space)
|
||||
|
||||
def assign_params(self, params_dict: Dict, category: str) -> None:
|
||||
"""
|
||||
|
@ -328,6 +339,20 @@ class Hyperopt:
|
|||
self.backtesting.strategy.trailing_only_offset_is_reached = \
|
||||
d['trailing_only_offset_is_reached']
|
||||
|
||||
if HyperoptTools.has_space(self.config, '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_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')
|
||||
|
||||
self.config.update({'max_open_trades': updated_max_open_trades})
|
||||
|
||||
self.backtesting.strategy.max_open_trades = updated_max_open_trades
|
||||
|
||||
with self.data_pickle_file.open('rb') as f:
|
||||
processed = load(f, mmap_mode='r')
|
||||
if self.analyze_per_epoch:
|
||||
|
@ -337,8 +362,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({
|
||||
|
|
|
@ -91,5 +91,8 @@ class HyperOptAuto(IHyperOpt):
|
|||
def trailing_space(self) -> List['Dimension']:
|
||||
return self._get_func('trailing_space')()
|
||||
|
||||
def max_open_trades_space(self) -> List['Dimension']:
|
||||
return self._get_func('max_open_trades_space')()
|
||||
|
||||
def generate_estimator(self, dimensions: List['Dimension'], **kwargs) -> EstimatorType:
|
||||
return self._get_func('generate_estimator')(dimensions=dimensions, **kwargs)
|
||||
|
|
|
@ -191,6 +191,16 @@ class IHyperOpt(ABC):
|
|||
Categorical([True, False], name='trailing_only_offset_is_reached'),
|
||||
]
|
||||
|
||||
def max_open_trades_space(self) -> List[Dimension]:
|
||||
"""
|
||||
Create a max open trades space.
|
||||
|
||||
You may override it in your custom Hyperopt class.
|
||||
"""
|
||||
return [
|
||||
Integer(-1, 10, name='max_open_trades'),
|
||||
]
|
||||
|
||||
# This is needed for proper unpickling the class attribute timeframe
|
||||
# which is set to the actual value by the resolver.
|
||||
# Why do I still need such shamanic mantras in modern python?
|
||||
|
|
|
@ -96,7 +96,7 @@ class HyperoptTools():
|
|||
Tell if the space value is contained in the configuration
|
||||
"""
|
||||
# 'trailing' and 'protection spaces are not included in the 'default' set of spaces
|
||||
if space in ('trailing', 'protection'):
|
||||
if space in ('trailing', 'protection', 'trades'):
|
||||
return any(s in config['spaces'] for s in [space, 'all'])
|
||||
else:
|
||||
return any(s in config['spaces'] for s in [space, 'all', 'default'])
|
||||
|
@ -187,7 +187,8 @@ class HyperoptTools():
|
|||
|
||||
if print_json:
|
||||
result_dict: Dict = {}
|
||||
for s in ['buy', 'sell', 'protection', 'roi', 'stoploss', 'trailing']:
|
||||
for s in ['buy', 'sell', 'protection',
|
||||
'roi', 'stoploss', 'trailing', 'max_open_trades']:
|
||||
HyperoptTools._params_update_for_json(result_dict, params, non_optimized, s)
|
||||
print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE))
|
||||
|
||||
|
@ -201,6 +202,8 @@ class HyperoptTools():
|
|||
HyperoptTools._params_pretty_print(params, 'roi', "ROI table:", non_optimized)
|
||||
HyperoptTools._params_pretty_print(params, 'stoploss', "Stoploss:", non_optimized)
|
||||
HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:", non_optimized)
|
||||
HyperoptTools._params_pretty_print(
|
||||
params, 'max_open_trades', "Max Open Trades:", non_optimized)
|
||||
|
||||
@staticmethod
|
||||
def _params_update_for_json(result_dict, params, non_optimized, space: str) -> None:
|
||||
|
@ -239,7 +242,9 @@ class HyperoptTools():
|
|||
if space == "stoploss":
|
||||
stoploss = safe_value_fallback2(space_params, no_params, space, space)
|
||||
result += (f"stoploss = {stoploss}{appendix}")
|
||||
|
||||
elif space == "max_open_trades":
|
||||
max_open_trades = safe_value_fallback2(space_params, no_params, space, space)
|
||||
result += (f"max_open_trades = {max_open_trades}{appendix}")
|
||||
elif space == "roi":
|
||||
result = result[:-1] + f'{appendix}\n'
|
||||
minimal_roi_result = rapidjson.dumps({
|
||||
|
|
|
@ -8,7 +8,7 @@ from pandas import DataFrame, to_datetime
|
|||
from tabulate import tabulate
|
||||
|
||||
from freqtrade.constants import (DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, UNLIMITED_STAKE_AMOUNT,
|
||||
Config)
|
||||
Config, IntOrInf)
|
||||
from freqtrade.data.metrics import (calculate_cagr, calculate_calmar, calculate_csum,
|
||||
calculate_expectancy, calculate_market_change,
|
||||
calculate_max_drawdown, calculate_sharpe, calculate_sortino)
|
||||
|
@ -191,7 +191,7 @@ def generate_tag_metrics(tag_type: str,
|
|||
return []
|
||||
|
||||
|
||||
def generate_exit_reason_stats(max_open_trades: int, results: DataFrame) -> List[Dict]:
|
||||
def generate_exit_reason_stats(max_open_trades: IntOrInf, results: DataFrame) -> List[Dict]:
|
||||
"""
|
||||
Generate small table outlining Backtest results
|
||||
:param max_open_trades: Max_open_trades parameter
|
||||
|
|
|
@ -76,6 +76,7 @@ class StrategyResolver(IResolver):
|
|||
("ignore_buying_expired_candle_after", 0),
|
||||
("position_adjustment_enable", False),
|
||||
("max_entry_position_adjustment", -1),
|
||||
("max_open_trades", -1)
|
||||
]
|
||||
for attribute, default in attributes:
|
||||
StrategyResolver._override_attribute_helper(strategy, config,
|
||||
|
@ -110,7 +111,11 @@ class StrategyResolver(IResolver):
|
|||
val = getattr(strategy, attribute)
|
||||
# None's cannot exist in the config, so do not copy them
|
||||
if val is not None:
|
||||
config[attribute] = val
|
||||
# max_open_trades set to -1 in the strategy will be copied as infinity in the config
|
||||
if attribute == 'max_open_trades' and val == -1:
|
||||
config[attribute] = float('inf')
|
||||
else:
|
||||
config[attribute] = val
|
||||
# Explicitly check for None here as other "falsy" values are possible
|
||||
elif default is not None:
|
||||
setattr(strategy, attribute, default)
|
||||
|
@ -128,6 +133,8 @@ class StrategyResolver(IResolver):
|
|||
key=lambda t: t[0]))
|
||||
if hasattr(strategy, 'stoploss'):
|
||||
strategy.stoploss = float(strategy.stoploss)
|
||||
if hasattr(strategy, 'max_open_trades') and strategy.max_open_trades < 0:
|
||||
strategy.max_open_trades = float('inf')
|
||||
return strategy
|
||||
|
||||
@staticmethod
|
||||
|
|
|
@ -3,7 +3,7 @@ from typing import Any, Dict, List, Optional, Union
|
|||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, IntOrInf
|
||||
from freqtrade.enums import OrderTypeValues, SignalDirection, TradingMode
|
||||
|
||||
|
||||
|
@ -165,7 +165,7 @@ class ShowConfig(BaseModel):
|
|||
stake_amount: str
|
||||
available_capital: Optional[float]
|
||||
stake_currency_decimals: int
|
||||
max_open_trades: int
|
||||
max_open_trades: IntOrInf
|
||||
minimal_roi: Dict[str, Any]
|
||||
stoploss: Optional[float]
|
||||
trailing_stop: Optional[bool]
|
||||
|
@ -422,7 +422,7 @@ class BacktestRequest(BaseModel):
|
|||
timeframe: Optional[str]
|
||||
timeframe_detail: Optional[str]
|
||||
timerange: Optional[str]
|
||||
max_open_trades: Optional[int]
|
||||
max_open_trades: Optional[IntOrInf]
|
||||
stake_amount: Optional[str]
|
||||
enable_protections: bool
|
||||
dry_run_wallet: Optional[float]
|
||||
|
|
|
@ -673,6 +673,7 @@ class RPC:
|
|||
if self._freqtrade.state == State.RUNNING:
|
||||
# Set 'max_open_trades' to 0
|
||||
self._freqtrade.config['max_open_trades'] = 0
|
||||
self._freqtrade.strategy.max_open_trades = 0
|
||||
|
||||
return {'status': 'No more entries will occur from now. Run /reload_config to reset.'}
|
||||
|
||||
|
|
|
@ -80,6 +80,8 @@ class HyperStrategyMixin:
|
|||
|
||||
self.stoploss = params.get('stoploss', {}).get(
|
||||
'stoploss', getattr(self, 'stoploss', -0.1))
|
||||
self.max_open_trades = params.get('max_open_trades', {}).get(
|
||||
'max_open_trades', getattr(self, 'max_open_trades', -1))
|
||||
trailing = params.get('trailing', {})
|
||||
self.trailing_stop = trailing.get(
|
||||
'trailing_stop', getattr(self, 'trailing_stop', False))
|
||||
|
|
|
@ -10,7 +10,7 @@ from typing import Dict, List, Optional, Tuple, Union
|
|||
import arrow
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.constants import Config, ListPairsWithTimeframes
|
||||
from freqtrade.constants import Config, IntOrInf, ListPairsWithTimeframes
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, RunMode, SignalDirection,
|
||||
SignalTagType, SignalType, TradingMode)
|
||||
|
@ -54,6 +54,9 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||
# associated stoploss
|
||||
stoploss: float
|
||||
|
||||
# max open trades for the strategy
|
||||
max_open_trades: IntOrInf
|
||||
|
||||
# trailing stoploss
|
||||
trailing_stop: bool = False
|
||||
trailing_stop_positive: Optional[float] = None
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
@ -685,6 +684,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'))
|
||||
|
@ -702,7 +703,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
|
||||
|
@ -786,6 +786,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
|
||||
|
@ -806,7 +808,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
|
||||
|
@ -860,6 +861,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.
|
||||
|
@ -872,7 +874,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
|
||||
|
@ -880,6 +881,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'))
|
||||
|
@ -897,7 +899,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
|
||||
|
@ -905,6 +906,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'))
|
||||
|
@ -928,7 +931,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,
|
||||
)
|
||||
|
||||
|
||||
|
@ -949,6 +951,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)
|
||||
|
@ -982,7 +985,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
|
||||
|
||||
|
@ -999,6 +1001,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'))
|
||||
|
@ -1025,7 +1028,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
|
||||
|
||||
|
@ -1063,11 +1065,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
|
||||
|
||||
|
@ -1078,7 +1081,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])
|
||||
|
@ -1095,6 +1098,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])
|
||||
|
@ -1108,6 +1112,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'
|
||||
|
@ -1166,6 +1171,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])
|
||||
|
@ -1174,11 +1180,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)
|
||||
|
@ -1196,11 +1202,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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
@ -7,6 +8,7 @@ import pandas as pd
|
|||
import pytest
|
||||
from arrow import Arrow
|
||||
from filelock import Timeout
|
||||
from skopt.space import Integer
|
||||
|
||||
from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt
|
||||
from freqtrade.data.history import load_data
|
||||
|
@ -292,6 +294,8 @@ def test_params_no_optimize_details(hyperopt) -> None:
|
|||
assert res['roi']['0'] == 0.04
|
||||
assert "stoploss" in res
|
||||
assert res['stoploss']['stoploss'] == -0.1
|
||||
assert "max_open_trades" in res
|
||||
assert res['max_open_trades']['max_open_trades'] == 1
|
||||
|
||||
|
||||
def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None:
|
||||
|
@ -334,8 +338,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")
|
||||
|
||||
|
||||
|
@ -474,6 +477,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
|
|||
'trailing_stop_positive': 0.02,
|
||||
'trailing_stop_positive_offset_p1': 0.05,
|
||||
'trailing_only_offset_is_reached': False,
|
||||
'max_open_trades': 3,
|
||||
}
|
||||
response_expected = {
|
||||
'loss': 1.9147239021396234,
|
||||
|
@ -499,7 +503,9 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
|
|||
'trailing': {'trailing_only_offset_is_reached': False,
|
||||
'trailing_stop': True,
|
||||
'trailing_stop_positive': 0.02,
|
||||
'trailing_stop_positive_offset': 0.07}},
|
||||
'trailing_stop_positive_offset': 0.07},
|
||||
'max_open_trades': {'max_open_trades': 3}
|
||||
},
|
||||
'params_dict': optimizer_param,
|
||||
'params_not_optimized': {'buy': {}, 'protection': {}, 'sell': {}},
|
||||
'results_metrics': ANY,
|
||||
|
@ -548,7 +554,8 @@ def test_print_json_spaces_all(mocker, hyperopt_conf, capsys) -> None:
|
|||
'buy': {'mfi-value': None},
|
||||
'sell': {'sell-mfi-value': None},
|
||||
'roi': {}, 'stoploss': {'stoploss': None},
|
||||
'trailing': {'trailing_stop': None}
|
||||
'trailing': {'trailing_stop': None},
|
||||
'max_open_trades': {'max_open_trades': None}
|
||||
},
|
||||
'results_metrics': generate_result_metrics(),
|
||||
}])
|
||||
|
@ -571,7 +578,7 @@ def test_print_json_spaces_all(mocker, hyperopt_conf, capsys) -> None:
|
|||
out, err = capsys.readouterr()
|
||||
result_str = (
|
||||
'{"params":{"mfi-value":null,"sell-mfi-value":null},"minimal_roi"'
|
||||
':{},"stoploss":null,"trailing_stop":null}'
|
||||
':{},"stoploss":null,"trailing_stop":null,"max_open_trades":null}'
|
||||
)
|
||||
assert result_str in out # noqa: E501
|
||||
# Should be called for historical candle data
|
||||
|
@ -702,8 +709,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")
|
||||
|
||||
|
||||
|
@ -776,8 +782,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")
|
||||
|
||||
|
||||
|
@ -819,8 +824,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")
|
||||
|
||||
|
||||
|
@ -874,6 +878,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.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)
|
||||
|
@ -884,6 +889,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.backtesting.strategy.max_open_trades != 1
|
||||
|
||||
hyperopt.custom_hyperopt.generate_estimator = lambda *args, **kwargs: 'ET1'
|
||||
with pytest.raises(OperationalException, match="Estimator ET1 not supported."):
|
||||
|
@ -984,3 +990,124 @@ 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_dump(mocker, hyperopt_conf, tmpdir, fee, capsys) -> None:
|
||||
# This test is to ensure that after hyperopting, max_open_trades is never
|
||||
# saved as inf in the output json params
|
||||
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'],
|
||||
})
|
||||
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)
|
||||
|
||||
hyperopt.start()
|
||||
|
||||
out, err = capsys.readouterr()
|
||||
|
||||
assert 'max_open_trades = -1' in out
|
||||
assert 'max_open_trades = inf' not in out
|
||||
|
||||
##############
|
||||
|
||||
hyperopt_conf.update({'print_json': True})
|
||||
|
||||
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)
|
||||
|
||||
hyperopt.start()
|
||||
|
||||
out, err = capsys.readouterr()
|
||||
|
||||
assert '"max_open_trades":-1' in out
|
||||
|
||||
|
||||
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)
|
||||
|
||||
hyperopt.custom_hyperopt.max_open_trades_space = lambda: [
|
||||
Integer(1, 10, name='max_open_trades')]
|
||||
|
||||
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
|
||||
|
|
|
@ -66,52 +66,58 @@ def test_load_previous_results2(mocker, testdatadir, caplog) -> None:
|
|||
@pytest.mark.parametrize("spaces, expected_results", [
|
||||
(['buy'],
|
||||
{'buy': True, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': False,
|
||||
'protection': False}),
|
||||
'protection': False, 'trades': False}),
|
||||
(['sell'],
|
||||
{'buy': False, 'sell': True, 'roi': False, 'stoploss': False, 'trailing': False,
|
||||
'protection': False}),
|
||||
'protection': False, 'trades': False}),
|
||||
(['roi'],
|
||||
{'buy': False, 'sell': False, 'roi': True, 'stoploss': False, 'trailing': False,
|
||||
'protection': False}),
|
||||
'protection': False, 'trades': False}),
|
||||
(['stoploss'],
|
||||
{'buy': False, 'sell': False, 'roi': False, 'stoploss': True, 'trailing': False,
|
||||
'protection': False}),
|
||||
'protection': False, 'trades': False}),
|
||||
(['trailing'],
|
||||
{'buy': False, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': True,
|
||||
'protection': False}),
|
||||
'protection': False, 'trades': False}),
|
||||
(['buy', 'sell', 'roi', 'stoploss'],
|
||||
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False,
|
||||
'protection': False}),
|
||||
'protection': False, 'trades': False}),
|
||||
(['buy', 'sell', 'roi', 'stoploss', 'trailing'],
|
||||
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True,
|
||||
'protection': False}),
|
||||
'protection': False, 'trades': False}),
|
||||
(['buy', 'roi'],
|
||||
{'buy': True, 'sell': False, 'roi': True, 'stoploss': False, 'trailing': False,
|
||||
'protection': False}),
|
||||
'protection': False, 'trades': False}),
|
||||
(['all'],
|
||||
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True,
|
||||
'protection': True}),
|
||||
'protection': True, 'trades': True}),
|
||||
(['default'],
|
||||
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False,
|
||||
'protection': False}),
|
||||
'protection': False, 'trades': False}),
|
||||
(['default', 'trailing'],
|
||||
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True,
|
||||
'protection': False}),
|
||||
'protection': False, 'trades': False}),
|
||||
(['all', 'buy'],
|
||||
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True,
|
||||
'protection': True}),
|
||||
'protection': True, 'trades': True}),
|
||||
(['default', 'buy'],
|
||||
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False,
|
||||
'protection': False}),
|
||||
'protection': False, 'trades': False}),
|
||||
(['all'],
|
||||
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True,
|
||||
'protection': True}),
|
||||
'protection': True, 'trades': True}),
|
||||
(['protection'],
|
||||
{'buy': False, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': False,
|
||||
'protection': True}),
|
||||
'protection': True, 'trades': False}),
|
||||
(['trades'],
|
||||
{'buy': False, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': False,
|
||||
'protection': False, 'trades': True}),
|
||||
(['default', 'trades'],
|
||||
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False,
|
||||
'protection': False, 'trades': True}),
|
||||
])
|
||||
def test_has_space(hyperopt_conf, spaces, expected_results):
|
||||
for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing', 'protection']:
|
||||
for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing', 'protection', 'trades']:
|
||||
hyperopt_conf.update({'spaces': spaces})
|
||||
assert HyperoptTools.has_space(hyperopt_conf, s) == expected_results[s]
|
||||
|
||||
|
@ -193,6 +199,9 @@ def test_export_params(tmpdir):
|
|||
"346": 0.08499,
|
||||
"507": 0.049,
|
||||
"1595": 0
|
||||
},
|
||||
"max_open_trades": {
|
||||
"max_open_trades": 5
|
||||
}
|
||||
},
|
||||
"params_not_optimized": {
|
||||
|
@ -219,6 +228,7 @@ def test_export_params(tmpdir):
|
|||
assert "roi" in content["params"]
|
||||
assert "stoploss" in content["params"]
|
||||
assert "trailing" in content["params"]
|
||||
assert "max_open_trades" in content["params"]
|
||||
|
||||
|
||||
def test_try_export_params(default_conf, tmpdir, caplog, mocker):
|
||||
|
@ -297,6 +307,9 @@ def test_params_print(capsys):
|
|||
"trailing_stop_positive_offset": 0.1,
|
||||
"trailing_only_offset_is_reached": True
|
||||
},
|
||||
"max_open_trades": {
|
||||
"max_open_trades": 5
|
||||
}
|
||||
|
||||
}
|
||||
HyperoptTools._params_pretty_print(params, 'buy', 'No header', non_optimized)
|
||||
|
@ -327,6 +340,13 @@ def test_params_print(capsys):
|
|||
assert re.search('trailing_stop_positive_offset = 0.1 # value loaded.*\n', captured.out)
|
||||
assert re.search('trailing_only_offset_is_reached = True # value loaded.*\n', captured.out)
|
||||
|
||||
HyperoptTools._params_pretty_print(
|
||||
params, 'max_open_trades', "Max Open Trades:", non_optimized)
|
||||
captured = capsys.readouterr()
|
||||
|
||||
assert re.search("# Max Open Trades:", captured.out)
|
||||
assert re.search('max_open_trades = 5 # value loaded.*\n', captured.out)
|
||||
|
||||
|
||||
def test_hyperopt_serializer():
|
||||
|
||||
|
|
|
@ -30,6 +30,9 @@ class StrategyTestV3(IStrategy):
|
|||
"0": 0.04
|
||||
}
|
||||
|
||||
# Optimal max_open_trades for the strategy
|
||||
max_open_trades = -1
|
||||
|
||||
# Optimal stoploss designed for the strategy
|
||||
stoploss = -0.10
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ from pathlib import Path
|
|||
import pytest
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.configuration import Configuration
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.resolvers import StrategyResolver
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
|
@ -175,6 +176,18 @@ def test_strategy_override_stoploss(caplog, default_conf):
|
|||
assert log_has("Override strategy 'stoploss' with value in config file: -0.5.", caplog)
|
||||
|
||||
|
||||
def test_strategy_override_max_open_trades(caplog, default_conf):
|
||||
caplog.set_level(logging.INFO)
|
||||
default_conf.update({
|
||||
'strategy': CURRENT_TEST_STRATEGY,
|
||||
'max_open_trades': 7
|
||||
})
|
||||
strategy = StrategyResolver.load_strategy(default_conf)
|
||||
|
||||
assert strategy.max_open_trades == 7
|
||||
assert log_has("Override strategy 'max_open_trades' with value in config file: 7.", caplog)
|
||||
|
||||
|
||||
def test_strategy_override_trailing_stop(caplog, default_conf):
|
||||
caplog.set_level(logging.INFO)
|
||||
default_conf.update({
|
||||
|
@ -349,6 +362,38 @@ def test_strategy_override_use_exit_profit_only(caplog, default_conf):
|
|||
assert log_has("Override strategy 'exit_profit_only' with value in config file: True.", caplog)
|
||||
|
||||
|
||||
def test_strategy_max_open_trades_infinity_from_strategy(caplog, default_conf):
|
||||
caplog.set_level(logging.INFO)
|
||||
default_conf.update({
|
||||
'strategy': CURRENT_TEST_STRATEGY,
|
||||
})
|
||||
del default_conf['max_open_trades']
|
||||
|
||||
strategy = StrategyResolver.load_strategy(default_conf)
|
||||
|
||||
# this test assumes -1 set to 'max_open_trades' in CURRENT_TEST_STRATEGY
|
||||
assert strategy.max_open_trades == float('inf')
|
||||
assert default_conf['max_open_trades'] == float('inf')
|
||||
|
||||
|
||||
def test_strategy_max_open_trades_infinity_from_config(caplog, default_conf, mocker):
|
||||
caplog.set_level(logging.INFO)
|
||||
default_conf.update({
|
||||
'strategy': CURRENT_TEST_STRATEGY,
|
||||
'max_open_trades': -1,
|
||||
'exchange': 'binance'
|
||||
})
|
||||
|
||||
configuration = Configuration(args=default_conf)
|
||||
parsed_config = configuration.get_config()
|
||||
|
||||
assert parsed_config['max_open_trades'] == float('inf')
|
||||
|
||||
strategy = StrategyResolver.load_strategy(parsed_config)
|
||||
|
||||
assert strategy.max_open_trades == float('inf')
|
||||
|
||||
|
||||
@ pytest.mark.filterwarnings("ignore:deprecated")
|
||||
def test_missing_implements(default_conf, caplog):
|
||||
|
||||
|
@ -438,3 +483,19 @@ def test_strategy_interface_versioning(dataframe_1m, default_conf):
|
|||
assert isinstance(exitdf, DataFrame)
|
||||
assert 'sell' not in exitdf
|
||||
assert 'exit_long' in exitdf
|
||||
|
||||
|
||||
def test_strategy_ft_load_params_from_file(mocker, default_conf):
|
||||
default_conf.update({'strategy': 'StrategyTestV2'})
|
||||
del default_conf['max_open_trades']
|
||||
mocker.patch('freqtrade.strategy.hyper.HyperStrategyMixin.load_params_from_file',
|
||||
return_value={
|
||||
'params': {
|
||||
'max_open_trades': {
|
||||
'max_open_trades': -1
|
||||
}
|
||||
}
|
||||
})
|
||||
strategy = StrategyResolver.load_strategy(default_conf)
|
||||
assert strategy.max_open_trades == float('inf')
|
||||
assert strategy.config['max_open_trades'] == float('inf')
|
||||
|
|
Loading…
Reference in New Issue
Block a user