From 3ff261e22cbd4a61a0388ff85acaab1c3c2f2116 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Mar 2022 07:09:01 +0100 Subject: [PATCH] Update order time in force to use entry/exit wording --- config_examples/config_full.example.json | 4 ++-- docs/configuration.md | 6 ++--- freqtrade/configuration/config_validation.py | 24 ++++++++++++++++++- freqtrade/constants.py | 8 +++---- freqtrade/freqtradebot.py | 16 ++++++------- freqtrade/optimize/backtesting.py | 4 ++-- freqtrade/resolvers/strategy_resolver.py | 5 +++- freqtrade/strategy/interface.py | 4 ++-- freqtrade/templates/base_strategy.py.j2 | 4 ++-- freqtrade/templates/sample_short_strategy.py | 4 ++-- freqtrade/templates/sample_strategy.py | 4 ++-- tests/optimize/test_backtesting.py | 1 + .../strategy/strats/hyperoptable_strategy.py | 4 ++-- tests/strategy/strats/strategy_test_v2.py | 4 ++-- tests/strategy/strats/strategy_test_v3.py | 4 ++-- tests/strategy/test_strategy_loading.py | 10 ++++---- tests/test_configuration.py | 22 +++++++++++++++++ 17 files changed, 88 insertions(+), 40 deletions(-) diff --git a/config_examples/config_full.example.json b/config_examples/config_full.example.json index 85c1bde5b..1fb2817b8 100644 --- a/config_examples/config_full.example.json +++ b/config_examples/config_full.example.json @@ -61,8 +61,8 @@ "stoploss_on_exchange_interval": 60 }, "order_time_in_force": { - "buy": "gtc", - "sell": "gtc" + "entry": "gtc", + "exit": "gtc" }, "pairlists": [ {"method": "StaticPairList"}, diff --git a/docs/configuration.md b/docs/configuration.md index 7a42966b0..99c13ca5a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -122,7 +122,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `ignore_roi_if_buy_signal` | Do not sell if the buy signal is still active. This setting takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
**Datatype:** Boolean | `ignore_buying_expired_candle_after` | Specifies the number of seconds until a buy signal is no longer used.
**Datatype:** Integer | `order_types` | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Dict -| `order_time_in_force` | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Dict +| `order_time_in_force` | Configure time in force for entry and exit orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Dict | `custom_price_max_distance_ratio` | Configure maximum distance ratio between current and custom entry or exit price.
*Defaults to `0.02` 2%).*
**Datatype:** Positive float | `exchange.name` | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename).
**Datatype:** String | `exchange.sandbox` | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details.
**Datatype:** Boolean @@ -465,8 +465,8 @@ The possible values are: `gtc` (default), `fok` or `ioc`. ``` python "order_time_in_force": { - "buy": "gtc", - "sell": "gtc" + "entry": "gtc", + "exit": "gtc" }, ``` diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index 85ff4408f..87a309f12 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -6,7 +6,7 @@ from jsonschema import Draft4Validator, validators from jsonschema.exceptions import ValidationError, best_match from freqtrade import constants -from freqtrade.enums import RunMode +from freqtrade.enums import RunMode, TradingMode from freqtrade.exceptions import OperationalException @@ -80,6 +80,7 @@ def validate_config_consistency(conf: Dict[str, Any]) -> None: _validate_protections(conf) _validate_unlimited_amount(conf) _validate_ask_orderbook(conf) + validate_migrated_strategy_settings(conf) # validate configuration before returning logger.info('Validating configuration ...') @@ -207,3 +208,24 @@ def _validate_ask_orderbook(conf: Dict[str, Any]) -> None: "Please use `order_book_top` instead of `order_book_min` and `order_book_max` " "for your `ask_strategy` configuration." ) + + +def validate_migrated_strategy_settings(conf: Dict[str, Any]) -> None: + + _validate_time_in_force(conf) + + +def _validate_time_in_force(conf: Dict[str, Any]) -> None: + + time_in_force = conf.get('order_time_in_force', {}) + if 'buy' in time_in_force or 'sell' in time_in_force: + if conf.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT: + raise OperationalException( + "Please migrate your time_in_force settings to use 'entry' and 'exit'.") + else: + logger.warning( + "DEPRECATED: Using 'buy' and 'sell' for time_in_force is deprecated." + "Please migrate your time_in_force settings to use 'entry' and 'exit'." + ) + time_in_force['entry'] = time_in_force.pop('buy') + time_in_force['exit'] = time_in_force.pop('sell') diff --git a/freqtrade/constants.py b/freqtrade/constants.py index cc4a14a2b..bafda93db 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -19,7 +19,7 @@ DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite' DEFAULT_DB_DRYRUN_URL = 'sqlite:///tradesv3.dryrun.sqlite' UNLIMITED_STAKE_AMOUNT = 'unlimited' DEFAULT_AMOUNT_RESERVE_PERCENT = 0.05 -REQUIRED_ORDERTIF = ['buy', 'sell'] +REQUIRED_ORDERTIF = ['entry', 'exit'] REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss', 'stoploss_on_exchange'] ORDERBOOK_SIDES = ['ask', 'bid'] ORDERTYPE_POSSIBILITIES = ['limit', 'market'] @@ -233,10 +233,10 @@ CONF_SCHEMA = { 'order_time_in_force': { 'type': 'object', 'properties': { - 'buy': {'type': 'string', 'enum': ORDERTIF_POSSIBILITIES}, - 'sell': {'type': 'string', 'enum': ORDERTIF_POSSIBILITIES} + 'entry': {'type': 'string', 'enum': ORDERTIF_POSSIBILITIES}, + 'exit': {'type': 'string', 'enum': ORDERTIF_POSSIBILITIES} }, - 'required': ['buy', 'sell'] + 'required': REQUIRED_ORDERTIF }, 'exchange': {'$ref': '#/definitions/exchange'}, 'edge': {'$ref': '#/definitions/edge'}, diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 341693982..4f3f723c0 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -595,7 +595,7 @@ class FreqtradeBot(LoggingMixin): :param leverage: amount of leverage applied to this trade :return: True if a buy order is created, false if it fails. """ - time_in_force = self.strategy.order_time_in_force['buy'] + time_in_force = self.strategy.order_time_in_force['entry'] [side, name] = ['sell', 'Short'] if is_short else ['buy', 'Long'] trade_side = 'short' if is_short else 'long' @@ -659,13 +659,12 @@ class FreqtradeBot(LoggingMixin): amount_requested = amount if order_status == 'expired' or order_status == 'rejected': - order_tif = self.strategy.order_time_in_force['buy'] # return false if the order is not filled if float(order['filled']) == 0: - logger.warning('%s %s order with time in force %s for %s is %s by %s.' - ' zero amount is fulfilled.', - name, order_tif, order_type, pair, order_status, self.exchange.name) + logger.warning(f'{name} {time_in_force} order with time in force {order_type} ' + f'for {pair} is {order_status} by {self.exchange.name}.' + ' zero amount is fulfilled.') return False else: # the order is partially fulfilled @@ -673,8 +672,9 @@ class FreqtradeBot(LoggingMixin): # if the order is fulfilled fully or partially logger.warning('%s %s order with time in force %s for %s is %s by %s.' ' %s amount fulfilled out of %s (%s remaining which is canceled).', - name, order_tif, order_type, pair, order_status, self.exchange.name, - order['filled'], order['amount'], order['remaining'] + name, time_in_force, order_type, pair, order_status, + self.exchange.name, order['filled'], order['amount'], + order['remaining'] ) stake_amount = order['cost'] amount = safe_value_fallback(order, 'filled', 'amount') @@ -1382,7 +1382,7 @@ class FreqtradeBot(LoggingMixin): order_type = self.strategy.order_types.get("emergencysell", "market") amount = self._safe_exit_amount(trade.pair, trade.amount) - time_in_force = self.strategy.order_time_in_force['sell'] + time_in_force = self.strategy.order_time_in_force['exit'] if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit, diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index fa3deb86f..744c77844 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -504,7 +504,7 @@ class Backtesting: # freqtrade does not support this in live, and the order would fill immediately closerate = max(closerate, sell_row[LOW_IDX]) # Confirm trade exit: - time_in_force = self.strategy.order_time_in_force['sell'] + time_in_force = self.strategy.order_time_in_force['exit'] if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount, @@ -640,7 +640,7 @@ class Backtesting: # If not pos adjust, trade is None return trade order_type = self.strategy.order_types['buy'] - time_in_force = self.strategy.order_time_in_force['buy'] + time_in_force = self.strategy.order_time_in_force['entry'] if not pos_adjust: max_leverage = self.exchange.get_max_leverage(pair, stake_amount) diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index fc4b71f1a..8dee459ba 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -10,6 +10,7 @@ from inspect import getfullargspec from pathlib import Path 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.exceptions import OperationalException from freqtrade.resolvers import IResolver @@ -160,10 +161,12 @@ class StrategyResolver(IResolver): @staticmethod def _strategy_sanity_validations(strategy): + # Ensure necessary migrations are performed first. + validate_migrated_strategy_settings(strategy.config) + if not all(k in strategy.order_types for k in REQUIRED_ORDERTYPES): raise ImportError(f"Impossible to load Strategy '{strategy.__class__.__name__}'. " f"Order-types mapping is incomplete.") - 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.") diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 92ea3daba..e5b583a9e 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -96,8 +96,8 @@ class IStrategy(ABC, HyperStrategyMixin): # Optional time in force order_time_in_force: Dict = { - 'buy': 'gtc', - 'sell': 'gtc', + 'entry': 'gtc', + 'exit': 'gtc', } # run "populate_indicators" only for new candle diff --git a/freqtrade/templates/base_strategy.py.j2 b/freqtrade/templates/base_strategy.py.j2 index 06abecc42..701909bf6 100644 --- a/freqtrade/templates/base_strategy.py.j2 +++ b/freqtrade/templates/base_strategy.py.j2 @@ -83,8 +83,8 @@ class {{ strategy }}(IStrategy): # Optional order time in force. order_time_in_force = { - 'buy': 'gtc', - 'sell': 'gtc' + 'entry': 'gtc', + 'exit': 'gtc' } {{ plot_config | indent(4) }} diff --git a/freqtrade/templates/sample_short_strategy.py b/freqtrade/templates/sample_short_strategy.py index bcb6c921e..c33327715 100644 --- a/freqtrade/templates/sample_short_strategy.py +++ b/freqtrade/templates/sample_short_strategy.py @@ -84,8 +84,8 @@ class SampleShortStrategy(IStrategy): # Optional order time in force. order_time_in_force = { - 'buy': 'gtc', - 'sell': 'gtc' + 'entry': 'gtc', + 'exit': 'gtc' } plot_config = { diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index 13df9c2a8..b3f1ae1c8 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -85,8 +85,8 @@ class SampleStrategy(IStrategy): # Optional order time in force. order_time_in_force = { - 'buy': 'gtc', - 'sell': 'gtc' + 'entry': 'gtc', + 'exit': 'gtc' } plot_config = { diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 1f0735907..ec77d1cbf 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -319,6 +319,7 @@ def test_backtesting_init_no_timeframe(mocker, default_conf, caplog) -> None: del default_conf['timeframe'] default_conf['strategy_list'] = [CURRENT_TEST_STRATEGY, 'SampleStrategy'] + # TODO: This refers to the sampleStrategy in user_data if it exists... mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5)) with pytest.raises(OperationalException): diff --git a/tests/strategy/strats/hyperoptable_strategy.py b/tests/strategy/strats/hyperoptable_strategy.py index 1126bd6cf..e843f6b58 100644 --- a/tests/strategy/strats/hyperoptable_strategy.py +++ b/tests/strategy/strats/hyperoptable_strategy.py @@ -45,8 +45,8 @@ class HyperoptableStrategy(IStrategy): # Optional time in force for orders order_time_in_force = { - 'buy': 'gtc', - 'sell': 'gtc', + 'entry': 'gtc', + 'exit': 'gtc', } buy_params = { diff --git a/tests/strategy/strats/strategy_test_v2.py b/tests/strategy/strats/strategy_test_v2.py index 59f1f569e..fd70cf346 100644 --- a/tests/strategy/strats/strategy_test_v2.py +++ b/tests/strategy/strats/strategy_test_v2.py @@ -47,8 +47,8 @@ class StrategyTestV2(IStrategy): # Optional time in force for orders order_time_in_force = { - 'buy': 'gtc', - 'sell': 'gtc', + 'entry': 'gtc', + 'exit': 'gtc', } # By default this strategy does not use Position Adjustments diff --git a/tests/strategy/strats/strategy_test_v3.py b/tests/strategy/strats/strategy_test_v3.py index 0b73c1271..962fd02e9 100644 --- a/tests/strategy/strats/strategy_test_v3.py +++ b/tests/strategy/strats/strategy_test_v3.py @@ -48,8 +48,8 @@ class StrategyTestV3(IStrategy): # Optional time in force for orders order_time_in_force = { - 'buy': 'gtc', - 'sell': 'gtc', + 'entry': 'gtc', + 'exit': 'gtc', } buy_params = { diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index da4f8fb78..8f407396c 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -257,8 +257,8 @@ def test_strategy_override_order_tif(caplog, default_conf): caplog.set_level(logging.INFO) order_time_in_force = { - 'buy': 'fok', - 'sell': 'gtc', + 'entry': 'fok', + 'exit': 'gtc', } default_conf.update({ @@ -268,15 +268,15 @@ def test_strategy_override_order_tif(caplog, default_conf): strategy = StrategyResolver.load_strategy(default_conf) assert strategy.order_time_in_force - for method in ['buy', 'sell']: + for method in ['entry', 'exit']: assert strategy.order_time_in_force[method] == order_time_in_force[method] assert log_has("Override strategy 'order_time_in_force' with value in config file:" - " {'buy': 'fok', 'sell': 'gtc'}.", caplog) + " {'entry': 'fok', 'exit': 'gtc'}.", caplog) default_conf.update({ 'strategy': CURRENT_TEST_STRATEGY, - 'order_time_in_force': {'buy': 'fok'} + 'order_time_in_force': {'entry': 'fok'} }) # Raise error for invalid configuration with pytest.raises(ImportError, diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 935421409..e2ab3c9b5 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -941,6 +941,28 @@ def test_validate_ask_orderbook(default_conf, caplog) -> None: validate_config_consistency(conf) +def test_validate_time_in_force(default_conf, caplog) -> None: + conf = deepcopy(default_conf) + conf['order_time_in_force'] = { + 'buy': 'gtc', + 'sell': 'gtc', + } + validate_config_consistency(conf) + assert log_has_re(r"DEPRECATED: Using 'buy' and 'sell' for time_in_force is.*", caplog) + assert conf['order_time_in_force']['entry'] == 'gtc' + assert conf['order_time_in_force']['exit'] == 'gtc' + + conf = deepcopy(default_conf) + conf['order_time_in_force'] = { + 'buy': 'gtc', + 'sell': 'gtc', + } + conf['trading_mode'] = 'futures' + with pytest.raises(OperationalException, + match=r"Please migrate your time_in_force settings .* 'entry' and 'exit'\."): + validate_config_consistency(conf) + + def test_load_config_test_comments() -> None: """ Load config with comments