Merge pull request #6589 from freqtrade/short_timeout

Short timeout
This commit is contained in:
Matthias 2022-03-26 12:57:00 +01:00 committed by GitHub
commit 5d3f2523e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 276 additions and 136 deletions

View File

@ -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"
},

View File

@ -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"
},

View File

@ -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"
},

View File

@ -30,8 +30,8 @@
},
"stoploss": -0.10,
"unfilledtimeout": {
"buy": 10,
"sell": 10,
"entry": 10,
"exit": 10,
"exit_timeout_count": 0,
"unit": "minutes"
},

View File

@ -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"
},

View File

@ -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

View File

@ -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). <br>*Defaults to `"spot"`.* <br> **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). <br> **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). <br>*Defaults to `0.05`.* <br> **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).<br> **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).<br> **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).<br> **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).<br> **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). <br> *Defaults to `minutes`.* <br> **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).<br>*Defaults to `0`.* <br> **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).<br> *Defaults to `bid`.* <br> **Datatype:** String (either `ask` or `bid`).

View File

@ -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]

View File

@ -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"
}
```

View File

@ -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)

View File

@ -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'}
}

View File

@ -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'])

View File

@ -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

View File

@ -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 "

View File

@ -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]

View File

@ -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)(

View File

@ -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"
},

View File

@ -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.

View File

@ -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,

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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])