diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 95efa63b8..b41a3ad9c 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -681,3 +681,4 @@ MakerTaker = Literal['maker', 'taker'] BidAsk = Literal['bid', 'ask'] Config = Dict[str, Any] +IntOrInf = float diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 3102683b2..0dcd05646 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -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 diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index d92f834df..3d560fd2a 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -1237,8 +1237,8 @@ class Backtesting: if not self.config.get('use_max_market_positions', True): logger.info( 'Ignoring max_open_trades (--disable-max-market-positions was used) ...') - self.strategy.max_open_trades = -1 - self.config.update({'max_open_trades': float('inf')}) + 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) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 595226fca..2e0dbdd65 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -120,8 +120,8 @@ class Hyperopt: # Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set if not self.config.get('use_max_market_positions', True): logger.debug('Ignoring max_open_trades (--disable-max-market-positions was used) ...') - self.backtesting.strategy.max_open_trades = -1 - config.update({'max_open_trades': float('inf')}) + 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 @@ -211,7 +211,8 @@ class Hyperopt: 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} + 'max_open_trades': self.backtesting.strategy.max_open_trades + if self.backtesting.strategy.max_open_trades != float('inf') else -1} return result @@ -344,16 +345,13 @@ class Hyperopt: # 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']) \ + 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') - 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_max_open_trades}) - self.config.update({'max_open_trades': updated_config_max_open_trades}) - - self.backtesting.strategy.max_open_trades = updated_strategy_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') diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 7de8f1a47..83f698fbe 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -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 diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index febda7822..e82aa7ac9 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -104,11 +104,7 @@ class StrategyResolver(IResolver): if (attribute in config and not isinstance(getattr(type(strategy), attribute, None), property)): # Ensure Properties are not overwritten - val = config[attribute] - # max_open_trades set to float('inf') in the config will be copied as -1 in the strategy - if attribute == 'max_open_trades' and val == float('inf'): - val = -1 - setattr(strategy, attribute, val) + setattr(strategy, attribute, config[attribute]) logger.info("Override strategy '%s' with value in config file: %s.", attribute, config[attribute]) elif hasattr(strategy, attribute): @@ -137,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 diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 404d64d16..d96055b69 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -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] diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index ed905d844..32563376c 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -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.'} diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 4d4e4f1ba..e6aed5c5a 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -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) @@ -55,7 +55,7 @@ class IStrategy(ABC, HyperStrategyMixin): stoploss: float # max open trades for the strategy - max_open_trades: int + max_open_trades: IntOrInf # trailing stoploss trailing_stop: bool = False diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index cf6faa0b9..4a8455fa7 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -1020,6 +1020,52 @@ def test_stake_amount_unlimited_max_open_trades(mocker, hyperopt_conf, tmpdir, f 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 diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index 2296d4bc6..d60d3ade9 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -6,6 +6,7 @@ from pathlib import Path import pytest from pandas import DataFrame +from freqtrade.configuration.configuration import Configuration from freqtrade.exceptions import OperationalException from freqtrade.resolvers import StrategyResolver from freqtrade.strategy.interface import IStrategy @@ -371,20 +372,26 @@ def test_strategy_max_open_trades_infinity_from_strategy(caplog, default_conf): strategy = StrategyResolver.load_strategy(default_conf) # this test assumes -1 set to 'max_open_trades' in CURRENT_TEST_STRATEGY - assert strategy.max_open_trades == -1 + 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): +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': float('inf') + 'max_open_trades': -1, + 'exchange': 'binance' }) - strategy = StrategyResolver.load_strategy(default_conf) + configuration = Configuration(args=default_conf) + parsed_config = configuration.get_config() - assert strategy.max_open_trades == -1 + 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") @@ -476,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')