diff --git a/config_examples/config_binance.example.json b/config_examples/config_binance.example.json index c6faf506c..ae84af420 100644 --- a/config_examples/config_binance.example.json +++ b/config_examples/config_binance.example.json @@ -8,8 +8,8 @@ "dry_run": true, "cancel_open_orders_on_exit": false, "unfilledtimeout": { - "buy": 10, - "sell": 10, + "entry": 10, + "exit": 10, "exit_timeout_count": 0, "unit": "minutes" }, diff --git a/config_examples/config_bittrex.example.json b/config_examples/config_bittrex.example.json index 9fe99c835..1f55d43ed 100644 --- a/config_examples/config_bittrex.example.json +++ b/config_examples/config_bittrex.example.json @@ -8,8 +8,8 @@ "dry_run": true, "cancel_open_orders_on_exit": false, "unfilledtimeout": { - "buy": 10, - "sell": 10, + "entry": 10, + "exit": 10, "exit_timeout_count": 0, "unit": "minutes" }, diff --git a/config_examples/config_ftx.example.json b/config_examples/config_ftx.example.json index 4f7c2af54..fbdff3333 100644 --- a/config_examples/config_ftx.example.json +++ b/config_examples/config_ftx.example.json @@ -8,8 +8,8 @@ "dry_run": true, "cancel_open_orders_on_exit": false, "unfilledtimeout": { - "buy": 10, - "sell": 10, + "entry": 10, + "exit": 10, "exit_timeout_count": 0, "unit": "minutes" }, diff --git a/config_examples/config_full.example.json b/config_examples/config_full.example.json index bbdafa805..9e4c342ed 100644 --- a/config_examples/config_full.example.json +++ b/config_examples/config_full.example.json @@ -30,8 +30,8 @@ }, "stoploss": -0.10, "unfilledtimeout": { - "buy": 10, - "sell": 10, + "entry": 10, + "exit": 10, "exit_timeout_count": 0, "unit": "minutes" }, diff --git a/config_examples/config_kraken.example.json b/config_examples/config_kraken.example.json index 5ac3a9255..27a4979d4 100644 --- a/config_examples/config_kraken.example.json +++ b/config_examples/config_kraken.example.json @@ -8,8 +8,8 @@ "dry_run": true, "cancel_open_orders_on_exit": false, "unfilledtimeout": { - "buy": 10, - "sell": 10, + "entry": 10, + "exit": 10, "exit_timeout_count": 0, "unit": "minutes" }, diff --git a/docs/bot-basics.md b/docs/bot-basics.md index f8d85a711..a44b611f3 100644 --- a/docs/bot-basics.md +++ b/docs/bot-basics.md @@ -32,8 +32,8 @@ By default, loop runs every few seconds (`internals.process_throttle_secs`) and * Call `populate_entry_trend()` * Call `populate_exit_trend()` * Check timeouts for open orders. - * Calls `check_buy_timeout()` strategy callback for open entry orders. - * Calls `check_sell_timeout()` strategy callback for open exit orders. + * Calls `check_entry_timeout()` strategy callback for open entry orders. + * Calls `check_exit_timeout()` strategy callback for open exit orders. * Verifies existing positions and eventually places exit orders. * Considers stoploss, ROI and exit-signal, `custom_exit()` and `custom_stoploss()`. * Determine exit-price based on `ask_strategy` configuration setting or by using the `custom_exit_price()` callback. @@ -64,7 +64,7 @@ This loop will be repeated again and again until the bot is stopped. * Check position adjustments for open trades if enabled and call `adjust_trade_position()` to determine if an additional order is requested. * Call `custom_stoploss()` and `custom_exit()` to find custom exit points. * For exits based on exit-signal and custom-exit: Call `custom_exit_price()` to determine exit price (Prices are moved to be within the closing candle). - * Check for Order timeouts, either via the `unfilledtimeout` configuration, or via `check_buy_timeout()` / `check_sell_timeout()` strategy callbacks. + * Check for Order timeouts, either via the `unfilledtimeout` configuration, or via `check_entry_timeout()` / `check_exit_timeout()` strategy callbacks. * Generate backtest report output !!! Note diff --git a/docs/configuration.md b/docs/configuration.md index 147f0b672..3f3086833 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -102,8 +102,8 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `trading_mode` | Specifies if you want to trade regularly, trade with leverage, or trade contracts whose prices are derived from matching cryptocurrency prices. [leverage documentation](leverage.md).
*Defaults to `"spot"`.*
**Datatype:** String | `margin_mode` | When trading with leverage, this determines if the collateral owned by the trader will be shared or isolated to each trading pair [leverage documentation](leverage.md).
**Datatype:** String | `liquidation_buffer` | A ratio specifying how large of a safety net to place between the liquidation price and the stoploss to prevent a position from reaching the liquidation price [leverage documentation](leverage.md).
*Defaults to `0.05`.*
**Datatype:** Float -| `unfilledtimeout.buy` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer -| `unfilledtimeout.sell` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer +| `unfilledtimeout.entry` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled entry order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer +| `unfilledtimeout.exit` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled exit order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer | `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy).
*Defaults to `minutes`.*
**Datatype:** String | `unfilledtimeout.exit_timeout_count` | How many times can exit orders time out. Once this number of timeouts is reached, an emergency sell is triggered. 0 to disable and allow unlimited order cancels. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `0`.*
**Datatype:** Integer | `bid_strategy.price_side` | Select the side of the spread the bot should look at to get the buy rate. [More information below](#buy-price-side).
*Defaults to `bid`.*
**Datatype:** String (either `ask` or `bid`). diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 6474ffcaa..6baf38c41 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -12,7 +12,7 @@ Currently available callbacks: * [`custom_exit()`](#custom-exit-signal) * [`custom_stoploss()`](#custom-stoploss) * [`custom_entry_price()` and `custom_exit_price()`](#custom-order-price-rules) -* [`check_buy_timeout()` and `check_sell_timeout()`](#custom-order-timeout-rules) +* [`check_entry_timeout()` and `check_exit_timeout()`](#custom-order-timeout-rules) * [`confirm_trade_entry()`](#trade-entry-buy-order-confirmation) * [`confirm_trade_exit()`](#trade-exit-sell-order-confirmation) * [`adjust_trade_position()`](#adjust-trade-position) @@ -408,7 +408,7 @@ However, freqtrade also offers a custom callback for both order types, which all ### Custom order timeout example Called for every open order until that order is either filled or cancelled. -`check_buy_timeout()` is called for trade entries, while `check_sell_timeout()` is called for trade exit orders. +`check_entry_timeout()` is called for trade entries, while `check_exit_timeout()` is called for trade exit orders. A simple example, which applies different unfilled-timeouts depending on the price of the asset can be seen below. It applies a tight timeout for higher priced assets, while allowing more time to fill on cheap coins. @@ -425,12 +425,12 @@ class AwesomeStrategy(IStrategy): # Set unfilledtimeout to 25 hours, since the maximum timeout from below is 24 hours. unfilledtimeout = { - 'buy': 60 * 25, - 'sell': 60 * 25 + 'entry': 60 * 25, + 'exit': 60 * 25 } - def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict, - current_time: datetime, **kwargs) -> bool: + def check_entry_timeout(self, pair: str, trade: 'Trade', order: dict, + current_time: datetime, **kwargs) -> bool: if trade.open_rate > 100 and trade.open_date_utc < current_time - timedelta(minutes=5): return True elif trade.open_rate > 10 and trade.open_date_utc < current_time - timedelta(minutes=3): @@ -440,7 +440,7 @@ class AwesomeStrategy(IStrategy): return False - def check_sell_timeout(self, pair: str, trade: Trade, order: dict, + def check_exit_timeout(self, pair: str, trade: Trade, order: dict, current_time: datetime, **kwargs) -> bool: if trade.open_rate > 100 and trade.open_date_utc < current_time - timedelta(minutes=5): return True @@ -466,12 +466,12 @@ class AwesomeStrategy(IStrategy): # Set unfilledtimeout to 25 hours, since the maximum timeout from below is 24 hours. unfilledtimeout = { - 'buy': 60 * 25, - 'sell': 60 * 25 + 'entry': 60 * 25, + 'exit': 60 * 25 } - def check_buy_timeout(self, pair: str, trade: Trade, order: dict, - current_time: datetime, **kwargs) -> bool: + def check_entry_timeout(self, pair: str, trade: Trade, order: dict, + current_time: datetime, **kwargs) -> bool: ob = self.dp.orderbook(pair, 1) current_price = ob['bids'][0][0] # Cancel buy order if price is more than 2% above the order. @@ -480,7 +480,7 @@ class AwesomeStrategy(IStrategy): return False - def check_sell_timeout(self, pair: str, trade: Trade, order: dict, + def check_exit_timeout(self, pair: str, trade: Trade, order: dict, current_time: datetime, **kwargs) -> bool: ob = self.dp.orderbook(pair, 1) current_price = ob['asks'][0][0] diff --git a/docs/strategy_migration.md b/docs/strategy_migration.md index 3a66ae9b3..ee6f1a494 100644 --- a/docs/strategy_migration.md +++ b/docs/strategy_migration.md @@ -11,6 +11,8 @@ If you intend on using markets other than spot markets, please migrate your stra * `populate_buy_trend()` -> `populate_entry_trend()` * `populate_sell_trend()` -> `populate_exit_trend()` * `custom_sell()` -> `custom_exit()` + * `check_buy_timeout()` -> `check_entry_timeout()` + * `check_sell_timeout()` -> `check_exit_timeout()` * Dataframe columns: * `buy` -> `enter_long` * `sell` -> `exit_long` @@ -30,6 +32,7 @@ If you intend on using markets other than spot markets, please migrate your stra * Strategy/Configuration settings. * `order_time_in_force` buy -> entry, sell -> exit. * `order_types` buy -> entry, sell -> exit. + * `unfilledtimeout` buy -> entry, sell -> exit. ## Extensive explanation @@ -124,6 +127,32 @@ class AwesomeStrategy(IStrategy): # ... ``` +### `custom_entry_timeout` + +`check_buy_timeout()` has been renamed to `check_entry_timeout()`, and `check_sell_timeout()` has been renamed to `check_exit_timeout()`. + +``` python hl_lines="2 6" +class AwesomeStrategy(IStrategy): + def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict, + current_time: datetime, **kwargs) -> bool: + return False + + def check_sell_timeout(self, pair: str, trade: 'Trade', order: dict, + current_time: datetime, **kwargs) -> bool: + return False +``` + +``` python hl_lines="2 6" +class AwesomeStrategy(IStrategy): + def check_entry_timeout(self, pair: str, trade: 'Trade', order: dict, + current_time: datetime, **kwargs) -> bool: + return False + + def check_exit_timeout(self, pair: str, trade: 'Trade', order: dict, + current_time: datetime, **kwargs) -> bool: + return False +``` + ### Custom-stake-amount New string argument `side` - which can be either `"long"` or `"short"`. @@ -259,6 +288,7 @@ This should be given the value of `trade.is_short`. "stoploss": "market", "stoploss_on_exchange": false, "stoploss_on_exchange_interval": 60 + } ``` ``` python hl_lines="2-6" @@ -271,4 +301,27 @@ This should be given the value of `trade.is_short`. "stoploss": "market", "stoploss_on_exchange": false, "stoploss_on_exchange_interval": 60 + } +``` + +#### `unfilledtimeout` + +`unfilledtimeout` have changed all wordings from `buy` to `entry` - and `sell` to `exit`. + +``` python hl_lines="2-3" +unfilledtimeout = { + "buy": 10, + "sell": 10, + "exit_timeout_count": 0, + "unit": "minutes" + } +``` + +``` python hl_lines="2-3" +unfilledtimeout = { + "entry": 10, + "exit": 10, + "exit_timeout_count": 0, + "unit": "minutes" + } ``` diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index 267509b43..3ebb18cd6 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -216,6 +216,7 @@ def validate_migrated_strategy_settings(conf: Dict[str, Any]) -> None: _validate_time_in_force(conf) _validate_order_types(conf) + _validate_unfilledtimeout(conf) def _validate_time_in_force(conf: Dict[str, Any]) -> None: @@ -258,3 +259,23 @@ def _validate_order_types(conf: Dict[str, Any]) -> None: ]: process_deprecated_setting(conf, 'order_types', o, 'order_types', n) + + +def _validate_unfilledtimeout(conf: Dict[str, Any]) -> None: + unfilledtimeout = conf.get('unfilledtimeout', {}) + if any(x in unfilledtimeout for x in ['buy', 'sell']): + if conf.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT: + raise OperationalException( + "Please migrate your unfilledtimeout settings to use the new wording.") + else: + + logger.warning( + "DEPRECATED: Using 'buy' and 'sell' for unfilledtimeout is deprecated." + "Please migrate your unfilledtimeout settings to use 'entry' and 'exit' wording." + ) + for o, n in [ + ('buy', 'entry'), + ('sell', 'exit'), + ]: + + process_deprecated_setting(conf, 'unfilledtimeout', o, 'unfilledtimeout', n) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index fabac5830..6441559ad 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -165,8 +165,8 @@ CONF_SCHEMA = { 'unfilledtimeout': { 'type': 'object', 'properties': { - 'buy': {'type': 'number', 'minimum': 1}, - 'sell': {'type': 'number', 'minimum': 1}, + 'entry': {'type': 'number', 'minimum': 1}, + 'exit': {'type': 'number', 'minimum': 1}, 'exit_timeout_count': {'type': 'number', 'minimum': 0, 'default': 0}, 'unit': {'type': 'string', 'enum': TIMEOUT_UNITS, 'default': 'minutes'} } diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f2cd5e5ad..089a5804a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1138,13 +1138,12 @@ class FreqtradeBot(LoggingMixin): fully_cancelled = self.update_trade_state(trade, trade.open_order_id, order) is_entering = order['side'] == trade.enter_side not_closed = order['status'] == 'open' or fully_cancelled - time_method = 'sell' if order['side'] == 'sell' else 'buy' max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0) order_obj = trade.select_order_by_order_id(trade.open_order_id) if not_closed and (fully_cancelled or (order_obj and self.strategy.ft_check_timed_out( - time_method, trade, order_obj, datetime.now(timezone.utc))) + trade, order_obj, datetime.now(timezone.utc))) ): if is_entering: self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT']) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index b50932cab..808e94bad 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -853,7 +853,7 @@ class Backtesting: """ for order in [o for o in trade.orders if o.ft_is_open]: - timedout = self.strategy.ft_check_timed_out(order.side, trade, order, current_time) + timedout = self.strategy.ft_check_timed_out(trade, order, current_time) if timedout: if order.side == trade.enter_side: self.timedout_entry_orders += 1 diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index c630fa967..87a9cc4b3 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -169,6 +169,51 @@ class StrategyResolver(IResolver): " in your strategy. Please note that short signals will be ignored in that case." ) + @staticmethod + def validate_strategy(strategy: IStrategy) -> IStrategy: + if strategy.config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT: + # Require new method + if not check_override(strategy, IStrategy, 'populate_entry_trend'): + raise OperationalException("`populate_entry_trend` must be implemented.") + if not check_override(strategy, IStrategy, 'populate_exit_trend'): + raise OperationalException("`populate_exit_trend` must be implemented.") + if check_override(strategy, IStrategy, 'check_buy_timeout'): + raise OperationalException("Please migrate your implementation " + "of `check_buy_timeout` to `check_entry_timeout`.") + if check_override(strategy, IStrategy, 'check_sell_timeout'): + raise OperationalException("Please migrate your implementation " + "of `check_sell_timeout` to `check_exit_timeout`.") + + if check_override(strategy, IStrategy, 'custom_sell'): + raise OperationalException( + "Please migrate your implementation of `custom_sell` to `custom_exit`.") + else: + # TODO: Implementing one of the following methods should show a deprecation warning + # buy_trend and sell_trend, custom_sell + if ( + not check_override(strategy, IStrategy, 'populate_buy_trend') + and not check_override(strategy, IStrategy, 'populate_entry_trend') + ): + raise OperationalException( + "`populate_entry_trend` or `populate_buy_trend` must be implemented.") + if ( + not check_override(strategy, IStrategy, 'populate_sell_trend') + and not check_override(strategy, IStrategy, 'populate_exit_trend') + ): + raise OperationalException( + "`populate_exit_trend` or `populate_sell_trend` must be implemented.") + + strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args) + strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args) + strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args) + if any(x == 2 for x in [ + strategy._populate_fun_len, + strategy._buy_fun_len, + strategy._sell_fun_len + ]): + strategy.INTERFACE_VERSION = 1 + return strategy + @staticmethod def _load_strategy(strategy_name: str, config: dict, extra_dir: Optional[str] = None) -> IStrategy: @@ -208,42 +253,8 @@ class StrategyResolver(IResolver): ) if strategy: - if strategy.config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT: - # Require new method - if not check_override(strategy, IStrategy, 'populate_entry_trend'): - raise OperationalException("`populate_entry_trend` must be implemented.") - if not check_override(strategy, IStrategy, 'populate_exit_trend'): - raise OperationalException("`populate_exit_trend` must be implemented.") - if check_override(strategy, IStrategy, 'custom_sell'): - raise OperationalException( - "Please migrate your implementation of `custom_sell` to `custom_exit`.") - else: - # TODO: Implementing one of the following methods should show a deprecation warning - # buy_trend and sell_trend, custom_sell - if ( - not check_override(strategy, IStrategy, 'populate_buy_trend') - and not check_override(strategy, IStrategy, 'populate_entry_trend') - ): - raise OperationalException( - "`populate_entry_trend` or `populate_buy_trend` must be implemented.") - if ( - not check_override(strategy, IStrategy, 'populate_sell_trend') - and not check_override(strategy, IStrategy, 'populate_exit_trend') - ): - raise OperationalException( - "`populate_exit_trend` or `populate_sell_trend` must be implemented.") - strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args) - strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args) - strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args) - if any(x == 2 for x in [ - strategy._populate_fun_len, - strategy._buy_fun_len, - strategy._sell_fun_len - ]): - strategy.INTERFACE_VERSION = 1 - - return strategy + return StrategyResolver.validate_strategy(strategy) raise OperationalException( f"Impossible to load Strategy '{strategy_name}'. This class does not exist " diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index b6d175c0f..07772647f 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -131,8 +131,8 @@ class Daily(BaseModel): class UnfilledTimeout(BaseModel): - buy: Optional[int] - sell: Optional[int] + entry: Optional[int] + exit: Optional[int] unit: Optional[str] exit_timeout_count: Optional[int] diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index da7f02912..c00eb238a 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -209,7 +209,14 @@ class IStrategy(ABC, HyperStrategyMixin): def check_buy_timeout(self, pair: str, trade: Trade, order: dict, current_time: datetime, **kwargs) -> bool: """ - Check buy timeout function callback. + DEPRECATED: Please use `check_entry_timeout` instead. + """ + return False + + def check_entry_timeout(self, pair: str, trade: Trade, order: dict, + current_time: datetime, **kwargs) -> bool: + """ + Check entry timeout function callback. This method can be used to override the enter-timeout. It is called whenever a limit entry order has been created, and is not yet fully filled. @@ -224,11 +231,19 @@ class IStrategy(ABC, HyperStrategyMixin): :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :return bool: When True is returned, then the entry order is cancelled. """ - return False + return self.check_buy_timeout( + pair=pair, trade=trade, order=order, current_time=current_time) def check_sell_timeout(self, pair: str, trade: Trade, order: dict, current_time: datetime, **kwargs) -> bool: """ + DEPRECATED: Please use `check_exit_timeout` instead. + """ + return False + + def check_exit_timeout(self, pair: str, trade: Trade, order: dict, + current_time: datetime, **kwargs) -> bool: + """ Check sell timeout function callback. This method can be used to override the exit-timeout. It is called whenever a (long) limit sell order or (short) limit buy @@ -244,7 +259,8 @@ class IStrategy(ABC, HyperStrategyMixin): :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :return bool: When True is returned, then the (long)sell/(short)buy-order is cancelled. """ - return False + return self.check_sell_timeout( + pair=pair, trade=trade, order=order, current_time=current_time) def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, time_in_force: str, current_time: datetime, entry_tag: Optional[str], @@ -1023,22 +1039,24 @@ class IStrategy(ABC, HyperStrategyMixin): else: return current_profit > roi - def ft_check_timed_out(self, side: str, trade: LocalTrade, order: Order, + def ft_check_timed_out(self, trade: LocalTrade, order: Order, current_time: datetime) -> bool: """ FT Internal method. Check if timeout is active, and if the order is still open and timed out """ + side = 'entry' if order.ft_order_side == trade.enter_side else 'exit' + timeout = self.config.get('unfilledtimeout', {}).get(side) if timeout is not None: timeout_unit = self.config.get('unfilledtimeout', {}).get('unit', 'minutes') timeout_kwargs = {timeout_unit: -timeout} timeout_threshold = current_time + timedelta(**timeout_kwargs) - timedout = (order.status == 'open' and order.side == side - and order.order_date_utc < timeout_threshold) + timedout = (order.status == 'open' and order.order_date_utc < timeout_threshold) if timedout: return True - time_method = self.check_sell_timeout if order.side == 'sell' else self.check_buy_timeout + time_method = (self.check_exit_timeout if order.side == trade.exit_side + else self.check_entry_timeout) return strategy_safe_wrapper(time_method, default_retval=False)( diff --git a/freqtrade/templates/base_config.json.j2 b/freqtrade/templates/base_config.json.j2 index 60f4b4fd7..e5f8f5efe 100644 --- a/freqtrade/templates/base_config.json.j2 +++ b/freqtrade/templates/base_config.json.j2 @@ -16,8 +16,8 @@ "trading_mode": "{{ trading_mode }}", "margin_mode": "{{ margin_mode }}", "unfilledtimeout": { - "buy": 10, - "sell": 10, + "entry": 10, + "exit": 10, "exit_timeout_count": 0, "unit": "minutes" }, diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 index 0ceeca982..d3178740b 100644 --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 @@ -170,11 +170,11 @@ def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: """ return True -def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool: +def check_entry_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool: """ - Check buy timeout function callback. - This method can be used to override the buy-timeout. - It is called whenever a limit buy order has been created, + Check entry timeout function callback. + This method can be used to override the entry-timeout. + It is called whenever a limit entry order has been created, and is not yet fully filled. Configuration options in `unfilledtimeout` will be verified before this, so ensure to set these timeouts high enough. @@ -190,11 +190,11 @@ def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> """ return False -def check_sell_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool: +def check_exit_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool: """ - Check sell timeout function callback. - This method can be used to override the sell-timeout. - It is called whenever a limit sell order has been created, + Check exit timeout function callback. + This method can be used to override the exit-timeout. + It is called whenever a limit exit order has been created, and is not yet fully filled. Configuration options in `unfilledtimeout` will be verified before this, so ensure to set these timeouts high enough. diff --git a/tests/conftest.py b/tests/conftest.py index 117aeaaed..898945370 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -416,8 +416,8 @@ def get_default_conf(testdatadir): "dry_run_wallet": 1000, "stoploss": -0.10, "unfilledtimeout": { - "buy": 10, - "sell": 30 + "entry": 10, + "exit": 30 }, "bid_strategy": { "ask_last_balance": 0.0, diff --git a/tests/strategy/strats/broken_strats/broken_futures_strategies.py b/tests/strategy/strats/broken_strats/broken_futures_strategies.py index 4a84b7491..7e6955d37 100644 --- a/tests/strategy/strats/broken_strats/broken_futures_strategies.py +++ b/tests/strategy/strats/broken_strats/broken_futures_strategies.py @@ -29,3 +29,21 @@ class TestStrategyImplementCustomSell(TestStrategyNoImplementSell): current_rate: float, current_profit: float, **kwargs): return False + + +class TestStrategyImplementBuyTimeout(TestStrategyNoImplementSell): + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + return super().populate_exit_trend(dataframe, metadata) + + def check_buy_timeout(self, pair: str, trade, order: dict, + current_time: datetime, **kwargs) -> bool: + return False + + +class TestStrategyImplementSellTimeout(TestStrategyNoImplementSell): + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + return super().populate_exit_trend(dataframe, metadata) + + def check_sell_timeout(self, pair: str, trade, order: dict, + current_time: datetime, **kwargs) -> bool: + return False diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index 89803e15d..b1b67dcf0 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -418,11 +418,20 @@ def test_missing_implements(default_conf): StrategyResolver.load_strategy(default_conf) default_conf['strategy'] = 'TestStrategyImplementCustomSell' - with pytest.raises(OperationalException, match=r"Please migrate your implementation of `custom_sell`.*"): StrategyResolver.load_strategy(default_conf) + default_conf['strategy'] = 'TestStrategyImplementBuyTimeout' + with pytest.raises(OperationalException, + match=r"Please migrate your implementation of `check_buy_timeout`.*"): + StrategyResolver.load_strategy(default_conf) + + default_conf['strategy'] = 'TestStrategyImplementSellTimeout' + with pytest.raises(OperationalException, + match=r"Please migrate your implementation of `check_sell_timeout`.*"): + StrategyResolver.load_strategy(default_conf) + @pytest.mark.filterwarnings("ignore:deprecated") def test_call_deprecated_function(result, default_conf, caplog): diff --git a/tests/test_configuration.py b/tests/test_configuration.py index e4a30c958..44187104a 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -963,7 +963,7 @@ def test_validate_time_in_force(default_conf, caplog) -> None: validate_config_consistency(conf) -def test_validate_order_types(default_conf, caplog) -> None: +def test__validate_order_types(default_conf, caplog) -> None: conf = deepcopy(default_conf) conf['order_types'] = { 'buy': 'limit', @@ -998,6 +998,31 @@ def test_validate_order_types(default_conf, caplog) -> None: validate_config_consistency(conf) +def test__validate_unfilledtimeout(default_conf, caplog) -> None: + conf = deepcopy(default_conf) + conf['unfilledtimeout'] = { + 'buy': 30, + 'sell': 35, + } + validate_config_consistency(conf) + assert log_has_re(r"DEPRECATED: Using 'buy' and 'sell' for unfilledtimeout is.*", caplog) + assert conf['unfilledtimeout']['entry'] == 30 + assert conf['unfilledtimeout']['exit'] == 35 + assert 'buy' not in conf['unfilledtimeout'] + assert 'sell' not in conf['unfilledtimeout'] + + conf = deepcopy(default_conf) + conf['unfilledtimeout'] = { + 'buy': 30, + 'sell': 35, + } + conf['trading_mode'] = 'futures' + with pytest.raises( + OperationalException, + match=r"Please migrate your unfilledtimeout settings to use the new wording\."): + validate_config_consistency(conf) + + def test_load_config_test_comments() -> None: """ Load config with comments diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 304f525bd..624399884 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2370,7 +2370,7 @@ def test_bot_loop_start_called_once(mocker, default_conf_usdt, caplog): @pytest.mark.parametrize("is_short", [False, True]) -def test_check_handle_timedout_buy_usercustom( +def test_check_handle_timedout_entry_usercustom( default_conf_usdt, ticker_usdt, limit_buy_order_old, open_trade, limit_sell_order_old, fee, mocker, is_short ) -> None: @@ -2378,8 +2378,7 @@ def test_check_handle_timedout_buy_usercustom( old_order = limit_sell_order_old if is_short else limit_buy_order_old old_order['id'] = open_trade.open_order_id - default_conf_usdt["unfilledtimeout"] = {"buy": 30, - "sell": 1400} if is_short else {"buy": 1400, "sell": 30} + default_conf_usdt["unfilledtimeout"] = {"entry": 1400, "exit": 30} rpc_mock = patch_RPCManager(mocker) cancel_order_mock = MagicMock(return_value=old_order) @@ -2399,6 +2398,7 @@ def test_check_handle_timedout_buy_usercustom( freqtrade = FreqtradeBot(default_conf_usdt) open_trade.is_short = is_short open_trade.orders[0].side = 'sell' if is_short else 'buy' + open_trade.orders[0].ft_order_side = 'sell' if is_short else 'buy' Trade.query.session.add(open_trade) # Ensure default is to return empty (so not mocked yet) @@ -2406,34 +2406,23 @@ def test_check_handle_timedout_buy_usercustom( assert cancel_order_mock.call_count == 0 # Return false - trade remains open - if is_short: - freqtrade.strategy.check_sell_timeout = MagicMock(return_value=False) - else: - freqtrade.strategy.check_buy_timeout = MagicMock(return_value=False) + freqtrade.strategy.check_entry_timeout = MagicMock(return_value=False) freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 0 trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() nb_trades = len(trades) assert nb_trades == 1 - if is_short: - assert freqtrade.strategy.check_sell_timeout.call_count == 1 - # Raise Keyerror ... (no impact on trade) - freqtrade.strategy.check_sell_timeout = MagicMock(side_effect=KeyError) - else: - assert freqtrade.strategy.check_buy_timeout.call_count == 1 - freqtrade.strategy.check_buy_timeout = MagicMock(side_effect=KeyError) + assert freqtrade.strategy.check_entry_timeout.call_count == 1 + freqtrade.strategy.check_entry_timeout = MagicMock(side_effect=KeyError) freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 0 trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() nb_trades = len(trades) assert nb_trades == 1 - if is_short: - assert freqtrade.strategy.check_sell_timeout.call_count == 1 - freqtrade.strategy.check_sell_timeout = MagicMock(return_value=True) - else: - assert freqtrade.strategy.check_buy_timeout.call_count == 1 - freqtrade.strategy.check_buy_timeout = MagicMock(return_value=True) + assert freqtrade.strategy.check_entry_timeout.call_count == 1 + freqtrade.strategy.check_entry_timeout = MagicMock(return_value=True) + # Trade should be closed since the function returns true freqtrade.check_handle_timedout() assert cancel_order_wr_mock.call_count == 1 @@ -2441,10 +2430,7 @@ def test_check_handle_timedout_buy_usercustom( trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() nb_trades = len(trades) assert nb_trades == 0 - if is_short: - assert freqtrade.strategy.check_sell_timeout.call_count == 1 - else: - assert freqtrade.strategy.check_buy_timeout.call_count == 1 + assert freqtrade.strategy.check_entry_timeout.call_count == 1 @pytest.mark.parametrize("is_short", [False, True]) @@ -2472,9 +2458,9 @@ def test_check_handle_timedout_buy( Trade.query.session.add(open_trade) if is_short: - freqtrade.strategy.check_sell_timeout = MagicMock(return_value=False) + freqtrade.strategy.check_exit_timeout = MagicMock(return_value=False) else: - freqtrade.strategy.check_buy_timeout = MagicMock(return_value=False) + freqtrade.strategy.check_entry_timeout = MagicMock(return_value=False) # check it does cancel buy orders over the time limit freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 1 @@ -2484,9 +2470,9 @@ def test_check_handle_timedout_buy( assert nb_trades == 0 # Custom user buy-timeout is never called if is_short: - assert freqtrade.strategy.check_sell_timeout.call_count == 0 + assert freqtrade.strategy.check_exit_timeout.call_count == 0 else: - assert freqtrade.strategy.check_buy_timeout.call_count == 0 + assert freqtrade.strategy.check_entry_timeout.call_count == 0 @pytest.mark.parametrize("is_short", [False, True]) @@ -2553,11 +2539,11 @@ def test_check_handle_timedout_buy_exception( @pytest.mark.parametrize("is_short", [False, True]) -def test_check_handle_timedout_sell_usercustom( +def test_check_handle_timedout_exit_usercustom( default_conf_usdt, ticker_usdt, limit_sell_order_old, mocker, is_short, open_trade_usdt, caplog ) -> None: - default_conf_usdt["unfilledtimeout"] = {"buy": 1440, "sell": 1440, "exit_timeout_count": 1} + default_conf_usdt["unfilledtimeout"] = {"entry": 1440, "exit": 1440, "exit_timeout_count": 1} limit_sell_order_old['id'] = open_trade_usdt.open_order_id if is_short: limit_sell_order_old['side'] = 'buy' @@ -2585,35 +2571,35 @@ def test_check_handle_timedout_sell_usercustom( freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 0 - freqtrade.strategy.check_sell_timeout = MagicMock(return_value=False) - freqtrade.strategy.check_buy_timeout = MagicMock(return_value=False) + freqtrade.strategy.check_exit_timeout = MagicMock(return_value=False) + freqtrade.strategy.check_entry_timeout = MagicMock(return_value=False) # Return false - No impact freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 0 assert rpc_mock.call_count == 0 assert open_trade_usdt.is_open is False - assert freqtrade.strategy.check_sell_timeout.call_count == (0 if is_short else 1) - assert freqtrade.strategy.check_buy_timeout.call_count == (1 if is_short else 0) + assert freqtrade.strategy.check_exit_timeout.call_count == 1 + assert freqtrade.strategy.check_entry_timeout.call_count == 0 - freqtrade.strategy.check_sell_timeout = MagicMock(side_effect=KeyError) - freqtrade.strategy.check_buy_timeout = MagicMock(side_effect=KeyError) + freqtrade.strategy.check_exit_timeout = MagicMock(side_effect=KeyError) + freqtrade.strategy.check_entry_timeout = MagicMock(side_effect=KeyError) # Return Error - No impact freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 0 assert rpc_mock.call_count == 0 assert open_trade_usdt.is_open is False - assert freqtrade.strategy.check_sell_timeout.call_count == (0 if is_short else 1) - assert freqtrade.strategy.check_buy_timeout.call_count == (1 if is_short else 0) + assert freqtrade.strategy.check_exit_timeout.call_count == 1 + assert freqtrade.strategy.check_entry_timeout.call_count == 0 # Return True - sells! - freqtrade.strategy.check_sell_timeout = MagicMock(return_value=True) - freqtrade.strategy.check_buy_timeout = MagicMock(return_value=True) + freqtrade.strategy.check_exit_timeout = MagicMock(return_value=True) + freqtrade.strategy.check_entry_timeout = MagicMock(return_value=True) freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 1 assert rpc_mock.call_count == 1 assert open_trade_usdt.is_open is True - assert freqtrade.strategy.check_sell_timeout.call_count == (0 if is_short else 1) - assert freqtrade.strategy.check_buy_timeout.call_count == (1 if is_short else 0) + assert freqtrade.strategy.check_exit_timeout.call_count == 1 + assert freqtrade.strategy.check_entry_timeout.call_count == 0 # 2nd canceled trade - Fail execute sell caplog.clear() @@ -2665,16 +2651,16 @@ def test_check_handle_timedout_sell( Trade.query.session.add(open_trade_usdt) - freqtrade.strategy.check_sell_timeout = MagicMock(return_value=False) - freqtrade.strategy.check_buy_timeout = MagicMock(return_value=False) + freqtrade.strategy.check_exit_timeout = MagicMock(return_value=False) + freqtrade.strategy.check_entry_timeout = MagicMock(return_value=False) # check it does cancel sell orders over the time limit freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 1 assert rpc_mock.call_count == 1 assert open_trade_usdt.is_open is True # Custom user sell-timeout is never called - assert freqtrade.strategy.check_sell_timeout.call_count == 0 - assert freqtrade.strategy.check_buy_timeout.call_count == 0 + assert freqtrade.strategy.check_exit_timeout.call_count == 0 + assert freqtrade.strategy.check_entry_timeout.call_count == 0 @pytest.mark.parametrize("is_short", [False, True])