diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 744c77844..8e354b2fc 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -127,10 +127,9 @@ class Backtesting: self.config['startup_candle_count'] = self.required_startup self.exchange.validate_required_startup_candles(self.required_startup, self.timeframe) - # TODO-lev: This should come from the configuration setting or better a - # TODO-lev: combination of config/strategy "use_shorts"(?) and "can_short" from the exchange self.trading_mode: TradingMode = config.get('trading_mode', TradingMode.SPOT) self.margin_mode: MarginMode = config.get('margin_mode', MarginMode.NONE) + # strategies which define "can_short=True" will fail to load in Spot mode. self._can_short = self.trading_mode != TradingMode.SPOT self.progress = BTProgress() diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 8dee459ba..7dfa6b0f2 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -12,6 +12,7 @@ from typing import Any, Dict, Optional from freqtrade.configuration.config_validation import validate_migrated_strategy_settings from freqtrade.constants import REQUIRED_ORDERTIF, REQUIRED_ORDERTYPES, USERPATH_STRATEGIES +from freqtrade.enums import TradingMode from freqtrade.exceptions import OperationalException from freqtrade.resolvers import IResolver from freqtrade.strategy.interface import IStrategy @@ -160,7 +161,7 @@ class StrategyResolver(IResolver): return strategy @staticmethod - def _strategy_sanity_validations(strategy): + def _strategy_sanity_validations(strategy: IStrategy): # Ensure necessary migrations are performed first. validate_migrated_strategy_settings(strategy.config) @@ -170,6 +171,15 @@ class StrategyResolver(IResolver): if not all(k in strategy.order_time_in_force for k in REQUIRED_ORDERTIF): raise ImportError(f"Impossible to load Strategy '{strategy.__class__.__name__}'. " f"Order-time-in-force mapping is incomplete.") + trading_mode = strategy.config.get('trading_mode', TradingMode.SPOT) + + if (strategy.can_short and trading_mode == TradingMode.SPOT): + raise ImportError( + "Short strategies cannot run in spot markets. Please make sure that this " + "is the correct strategy and that your trading mode configuration is correct. " + "You can run this strategy in spot markets by setting `can_short=False`" + " in your strategy. Please note that short signals will be ignored in that case." + ) @staticmethod def _load_strategy(strategy_name: str, diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index e5b583a9e..bec89131b 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -81,6 +81,9 @@ class IStrategy(ABC, HyperStrategyMixin): trailing_only_offset_is_reached = False use_custom_stoploss: bool = False + # Can this strategy go short? + can_short: bool = False + # associated timeframe ticker_interval: str # DEPRECATED timeframe: str @@ -766,6 +769,7 @@ class IStrategy(ABC, HyperStrategyMixin): enter_signal = SignalDirection.LONG enter_tag_value = latest.get(SignalTagType.ENTER_TAG.value, None) if (self.config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT + and self.can_short and enter_short == 1 and not any([exit_short, enter_long])): enter_signal = SignalDirection.SHORT enter_tag_value = latest.get(SignalTagType.ENTER_TAG.value, None) diff --git a/freqtrade/templates/base_strategy.py.j2 b/freqtrade/templates/base_strategy.py.j2 index 701909bf6..0a20eaf2a 100644 --- a/freqtrade/templates/base_strategy.py.j2 +++ b/freqtrade/templates/base_strategy.py.j2 @@ -40,6 +40,9 @@ class {{ strategy }}(IStrategy): # Optimal timeframe for the strategy. timeframe = '5m' + # Can this strategy go short? + can_short: bool = False + # Minimal ROI designed for the strategy. # This attribute will be overridden if the config file contains "minimal_roi". minimal_roi = { diff --git a/freqtrade/templates/sample_short_strategy.py b/freqtrade/templates/sample_short_strategy.py index c33327715..535c2222d 100644 --- a/freqtrade/templates/sample_short_strategy.py +++ b/freqtrade/templates/sample_short_strategy.py @@ -38,6 +38,9 @@ class SampleShortStrategy(IStrategy): # Check the documentation or the Sample strategy to get the latest version. INTERFACE_VERSION = 2 + # Can this strategy go short? + can_short: bool = True + # Minimal ROI designed for the strategy. # This attribute will be overridden if the config file contains "minimal_roi". minimal_roi = { diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index b3f1ae1c8..3da92fa0b 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -37,6 +37,9 @@ class SampleStrategy(IStrategy): # Check the documentation or the Sample strategy to get the latest version. INTERFACE_VERSION = 2 + # Can this strategy go short? + can_short: bool = False + # Minimal ROI designed for the strategy. # This attribute will be overridden if the config file contains "minimal_roi". minimal_roi = { @@ -55,12 +58,6 @@ class SampleStrategy(IStrategy): # trailing_stop_positive = 0.01 # trailing_stop_positive_offset = 0.0 # Disabled / not configured - # Hyperoptable parameters - buy_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True) - sell_rsi = IntParameter(low=50, high=100, default=70, space='sell', optimize=True, load=True) - short_rsi = IntParameter(low=51, high=100, default=70, space='sell', optimize=True, load=True) - exit_short_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True) - # Optimal timeframe for the strategy. timeframe = '5m' @@ -72,6 +69,12 @@ class SampleStrategy(IStrategy): sell_profit_only = False ignore_roi_if_buy_signal = False + # Hyperoptable parameters + buy_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True) + sell_rsi = IntParameter(low=50, high=100, default=70, space='sell', optimize=True, load=True) + short_rsi = IntParameter(low=51, high=100, default=70, space='sell', optimize=True, load=True) + exit_short_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True) + # Number of candles the strategy requires before producing valid signals startup_candle_count: int = 30 diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 4050dcbdb..61cdfb2bc 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1382,6 +1382,7 @@ def test_api_strategies(botclient): 'InformativeDecoratorTest', 'StrategyTestV2', 'StrategyTestV3', + 'StrategyTestV3Futures', 'TestStrategyLegacyV1', ]} diff --git a/tests/strategy/strats/strategy_test_v3.py b/tests/strategy/strats/strategy_test_v3.py index 962fd02e9..ee3ce5773 100644 --- a/tests/strategy/strats/strategy_test_v3.py +++ b/tests/strategy/strats/strategy_test_v3.py @@ -187,3 +187,7 @@ class StrategyTestV3(IStrategy): return round(orders[0].cost, 0) return None + + +class StrategyTestV3Futures(StrategyTestV3): + can_short = True diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 82cc707f4..4a01b7dec 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -78,6 +78,11 @@ def test_returns_latest_signal(ohlcv_history): assert _STRATEGY.get_entry_signal('ETH/BTC', '5m', mocked_history) == (None, None) _STRATEGY.config['trading_mode'] = 'futures' + # Short signal get's ignored as can_short is not set. + assert _STRATEGY.get_entry_signal('ETH/BTC', '5m', mocked_history) == (None, None) + + _STRATEGY.can_short = True + assert _STRATEGY.get_entry_signal( 'ETH/BTC', '5m', mocked_history) == (SignalDirection.SHORT, 'sell_signal_01') assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history) == (False, False, None) @@ -93,6 +98,7 @@ def test_returns_latest_signal(ohlcv_history): assert _STRATEGY.get_exit_signal( 'ETH/BTC', '5m', mocked_history, True) == (False, True, 'sell_signal_02') + _STRATEGY.can_short = False _STRATEGY.config['trading_mode'] = 'spot' diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index 8f407396c..b8fe90e23 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -35,7 +35,7 @@ def test_search_all_strategies_no_failed(): directory = Path(__file__).parent / "strats" strategies = StrategyResolver.search_all_objects(directory, enum_failed=False) assert isinstance(strategies, list) - assert len(strategies) == 5 + assert len(strategies) == 6 assert isinstance(strategies[0], dict) @@ -43,10 +43,10 @@ def test_search_all_strategies_with_failed(): directory = Path(__file__).parent / "strats" strategies = StrategyResolver.search_all_objects(directory, enum_failed=True) assert isinstance(strategies, list) - assert len(strategies) == 6 + assert len(strategies) == 7 # with enum_failed=True search_all_objects() shall find 2 good strategies # and 1 which fails to load - assert len([x for x in strategies if x['class'] is not None]) == 5 + assert len([x for x in strategies if x['class'] is not None]) == 6 assert len([x for x in strategies if x['class'] is None]) == 1 @@ -128,6 +128,22 @@ def test_strategy_pre_v3(result, default_conf, strategy_name): assert 'exit_long' in dataframe.columns +def test_strategy_can_short(caplog, default_conf): + caplog.set_level(logging.INFO) + default_conf.update({ + 'strategy': CURRENT_TEST_STRATEGY, + }) + strat = StrategyResolver.load_strategy(default_conf) + assert isinstance(strat, IStrategy) + default_conf['strategy'] = 'StrategyTestV3Futures' + with pytest.raises(ImportError, match=""): + StrategyResolver.load_strategy(default_conf) + + default_conf['trading_mode'] = 'futures' + strat = StrategyResolver.load_strategy(default_conf) + assert isinstance(strat, IStrategy) + + def test_strategy_override_minimal_roi(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({