diff --git a/config_binance.json.example b/config_binance.json.example index 4fa615d6d..b13a5725c 100644 --- a/config_binance.json.example +++ b/config_binance.json.example @@ -13,7 +13,7 @@ }, "bid_strategy": { "ask_last_balance": 0.0, - "use_order_book": false, + "use_order_book": true, "order_book_top": 1, "check_depth_of_market": { "enabled": false, @@ -21,9 +21,8 @@ } }, "ask_strategy": { - "use_order_book": false, - "order_book_min": 1, - "order_book_max": 1, + "use_order_book": true, + "order_book_top": 1, "use_sell_signal": true, "sell_profit_only": false, "ignore_roi_if_buy_signal": false diff --git a/config_bittrex.json.example b/config_bittrex.json.example index 172cfcfc3..9a0c62021 100644 --- a/config_bittrex.json.example +++ b/config_bittrex.json.example @@ -12,7 +12,7 @@ "sell": 30 }, "bid_strategy": { - "use_order_book": false, + "use_order_book": true, "ask_last_balance": 0.0, "order_book_top": 1, "check_depth_of_market": { @@ -21,9 +21,8 @@ } }, "ask_strategy":{ - "use_order_book": false, - "order_book_min": 1, - "order_book_max": 1, + "use_order_book": true, + "order_book_top": 1, "use_sell_signal": true, "sell_profit_only": false, "ignore_roi_if_buy_signal": false diff --git a/config_ftx.json.example b/config_ftx.json.example index facd54b25..f1ad0a4f3 100644 --- a/config_ftx.json.example +++ b/config_ftx.json.example @@ -13,7 +13,7 @@ }, "bid_strategy": { "ask_last_balance": 0.0, - "use_order_book": false, + "use_order_book": true, "order_book_top": 1, "check_depth_of_market": { "enabled": false, @@ -21,9 +21,8 @@ } }, "ask_strategy": { - "use_order_book": false, - "order_book_min": 1, - "order_book_max": 1, + "use_order_book": true, + "order_book_top": 1, "use_sell_signal": true, "sell_profit_only": false, "ignore_roi_if_buy_signal": false diff --git a/config_full.json.example b/config_full.json.example index bc9f33f96..5b0448f13 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -28,7 +28,7 @@ }, "bid_strategy": { "price_side": "bid", - "use_order_book": false, + "use_order_book": true, "ask_last_balance": 0.0, "order_book_top": 1, "check_depth_of_market": { @@ -38,9 +38,8 @@ }, "ask_strategy":{ "price_side": "ask", - "use_order_book": false, - "order_book_min": 1, - "order_book_max": 1, + "use_order_book": true, + "order_book_top": 1, "use_sell_signal": true, "sell_profit_only": false, "sell_profit_offset": 0.0, diff --git a/config_kraken.json.example b/config_kraken.json.example index 3cd90e5d3..9fe094120 100644 --- a/config_kraken.json.example +++ b/config_kraken.json.example @@ -12,7 +12,7 @@ "sell": 30 }, "bid_strategy": { - "use_order_book": false, + "use_order_book": true, "ask_last_balance": 0.0, "order_book_top": 1, "check_depth_of_market": { @@ -21,9 +21,8 @@ } }, "ask_strategy":{ - "use_order_book": false, - "order_book_min": 1, - "order_book_max": 1, + "use_order_book": true, + "order_book_top": 1, "use_sell_signal": true, "sell_profit_only": false, "ignore_roi_if_buy_signal": false diff --git a/docs/configuration.md b/docs/configuration.md index a2ed41b99..074b9ddf0 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -74,14 +74,13 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `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`). | `bid_strategy.ask_last_balance` | **Required.** Interpolate the bidding price. More information [below](#buy-price-without-orderbook-enabled). | `bid_strategy.use_order_book` | Enable buying using the rates in [Order Book Bids](#buy-price-with-orderbook-enabled).
**Datatype:** Boolean -| `bid_strategy.order_book_top` | Bot will use the top N rate in Order Book Bids to buy. I.e. a value of 2 will allow the bot to pick the 2nd bid rate in [Order Book Bids](#buy-price-with-orderbook-enabled).
*Defaults to `1`.*
**Datatype:** Positive Integer +| `bid_strategy.order_book_top` | Bot will use the top N rate in Order Book "price_side" to buy. I.e. a value of 2 will allow the bot to pick the 2nd bid rate in [Order Book Bids](#buy-price-with-orderbook-enabled).
*Defaults to `1`.*
**Datatype:** Positive Integer | `bid_strategy. check_depth_of_market.enabled` | Do not buy if the difference of buy orders and sell orders is met in Order Book. [Check market depth](#check-depth-of-market).
*Defaults to `false`.*
**Datatype:** Boolean | `bid_strategy. check_depth_of_market.bids_to_ask_delta` | The difference ratio of buy orders and sell orders found in Order Book. A value below 1 means sell order size is greater, while value greater than 1 means buy order size is higher. [Check market depth](#check-depth-of-market)
*Defaults to `0`.*
**Datatype:** Float (as ratio) | `ask_strategy.price_side` | Select the side of the spread the bot should look at to get the sell rate. [More information below](#sell-price-side).
*Defaults to `ask`.*
**Datatype:** String (either `ask` or `bid`). | `ask_strategy.bid_last_balance` | Interpolate the selling price. More information [below](#sell-price-without-orderbook-enabled). | `ask_strategy.use_order_book` | Enable selling of open trades using [Order Book Asks](#sell-price-with-orderbook-enabled).
**Datatype:** Boolean -| `ask_strategy.order_book_min` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
*Defaults to `1`.*
**Datatype:** Positive Integer -| `ask_strategy.order_book_max` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
*Defaults to `1`.*
**Datatype:** Positive Integer +| `ask_strategy.order_book_top` | Bot will use the top N rate in Order Book "price_side" to sell. I.e. a value of 2 will allow the bot to pick the 2nd ask rate in [Order Book Asks](#sell-price-with-orderbook-enabled)
*Defaults to `1`.*
**Datatype:** Positive Integer | `ask_strategy.use_sell_signal` | Use sell signals produced by the strategy in addition to the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `true`.*
**Datatype:** Boolean | `ask_strategy.sell_profit_only` | Wait until the bot reaches `ask_strategy.sell_profit_offset` before taking a sell decision. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
**Datatype:** Boolean | `ask_strategy.sell_profit_offset` | Sell-signal is only active above this value. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `0.0`.*
**Datatype:** Float (as ratio) diff --git a/docs/deprecated.md b/docs/deprecated.md index 312f2c74f..b7ad847e6 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -33,3 +33,8 @@ The old section of configuration parameters (`"pairlist"`) has been deprecated i ### deprecation of bidVolume and askVolume from volume-pairlist Since only quoteVolume can be compared between assets, the other options (bidVolume, askVolume) have been deprecated in 2020.4, and have been removed in 2020.9. + +### Using order book steps for sell price + +Using `order_book_min` and `order_book_max` used to allow stepping the orderbook and trying to find the next ROI slot - trying to place sell-orders early. +As this does however increase risk and provides no benefit, it's been removed for maintainability purposes in 2021.7. diff --git a/docs/includes/pricing.md b/docs/includes/pricing.md index bdf27eb20..ed8a45e68 100644 --- a/docs/includes/pricing.md +++ b/docs/includes/pricing.md @@ -47,7 +47,7 @@ Also, prices at the "ask" side of the spread are higher than prices at the "bid" #### Buy price with Orderbook enabled -When buying with the orderbook enabled (`bid_strategy.use_order_book=True`), Freqtrade fetches the `bid_strategy.order_book_top` entries from the orderbook and then uses the entry specified as `bid_strategy.order_book_top` on the configured side (`bid_strategy.price_side`) of the orderbook. 1 specifies the topmost entry in the orderbook, while 2 would use the 2nd entry in the orderbook, and so on. +When buying with the orderbook enabled (`bid_strategy.use_order_book=True`), Freqtrade fetches the `bid_strategy.order_book_top` entries from the orderbook and uses the entry specified as `bid_strategy.order_book_top` on the configured side (`bid_strategy.price_side`) of the orderbook. 1 specifies the topmost entry in the orderbook, while 2 would use the 2nd entry in the orderbook, and so on. #### Buy price without Orderbook enabled @@ -82,22 +82,9 @@ In line with that, if `ask_strategy.price_side` is set to `"bid"`, then the bot #### Sell price with Orderbook enabled -When selling with the orderbook enabled (`ask_strategy.use_order_book=True`), Freqtrade fetches the `ask_strategy.order_book_max` entries in the orderbook. Then each of the orderbook steps between `ask_strategy.order_book_min` and `ask_strategy.order_book_max` on the configured orderbook side are validated for a profitable sell-possibility based on the strategy configuration (`minimal_roi` conditions) and the sell order is placed at the first profitable spot. +When selling with the orderbook enabled (`ask_strategy.use_order_book=True`), Freqtrade fetches the `ask_strategy.order_book_top` entries in the orderbook and uses the entry specified as `ask_strategy.order_book_top` from the configured side (`ask_strategy.price_side`) as selling price. -!!! Note - Using `order_book_max` higher than `order_book_min` only makes sense when ask_strategy.price_side is set to `"ask"`. - -The idea here is to place the sell order early, to be ahead in the queue. - -A fixed slot (mirroring `bid_strategy.order_book_top`) can be defined by setting `ask_strategy.order_book_min` and `ask_strategy.order_book_max` to the same number. - -!!! Warning "Order_book_max > 1 - increased risks for stoplosses!" - Using `ask_strategy.order_book_max` higher than 1 will increase the risk the stoploss on exchange is cancelled too early, since an eventual [stoploss on exchange](#understand-order_types) will be cancelled as soon as the order is placed. - Also, the sell order will remain on the exchange for `unfilledtimeout.sell` (or until it's filled) - which can lead to missed stoplosses (with or without using stoploss on exchange). - -!!! Warning "Order_book_max > 1 in dry-run" - Using `ask_strategy.order_book_max` higher than 1 will result in improper dry-run results (significantly better than real orders executed on exchange), since dry-run assumes orders to be filled almost instantly. - It is therefore advised to not use this setting for dry-runs. +1 specifies the topmost entry in the orderbook, while 2 would use the 2nd entry in the orderbook, and so on. #### Sell price without Orderbook enabled diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index 3004d6bf7..29c078d0d 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -79,6 +79,7 @@ def validate_config_consistency(conf: Dict[str, Any]) -> None: _validate_whitelist(conf) _validate_protections(conf) _validate_unlimited_amount(conf) + _validate_ask_orderbook(conf) # validate configuration before returning logger.info('Validating configuration ...') @@ -186,3 +187,23 @@ def _validate_protections(conf: Dict[str, Any]) -> None: "Protections must specify either `lookback_period` or `lookback_period_candles`.\n" f"Please fix the protection {prot.get('method')}" ) + + +def _validate_ask_orderbook(conf: Dict[str, Any]) -> None: + ask_strategy = conf.get('ask_strategy', {}) + ob_min = ask_strategy.get('order_book_min') + ob_max = ask_strategy.get('order_book_max') + if ob_min is not None and ob_max is not None and ask_strategy.get('use_order_book'): + if ob_min != ob_max: + raise OperationalException( + "Using order_book_max != order_book_min in ask_strategy is no longer supported." + "Please pick one value and use `order_book_top` in the future." + ) + else: + # Move value to order_book_top + ask_strategy['order_book_top'] = ob_min + logger.warning( + "DEPRECATED: " + "Please use `order_book_top` instead of `order_book_min` and `order_book_max` " + "for your `ask_strategy` configuration." + ) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 6ee4047d2..364eb54bd 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -154,7 +154,7 @@ CONF_SCHEMA = { }, 'price_side': {'type': 'string', 'enum': ORDERBOOK_SIDES, 'default': 'bid'}, 'use_order_book': {'type': 'boolean'}, - 'order_book_top': {'type': 'integer', 'maximum': 20, 'minimum': 1}, + 'order_book_top': {'type': 'integer', 'minimum': 1, 'maximum': 50, }, 'check_depth_of_market': { 'type': 'object', 'properties': { @@ -163,7 +163,7 @@ CONF_SCHEMA = { } }, }, - 'required': ['ask_last_balance'] + 'required': ['price_side'] }, 'ask_strategy': { 'type': 'object', @@ -176,13 +176,13 @@ CONF_SCHEMA = { 'exclusiveMaximum': False, }, 'use_order_book': {'type': 'boolean'}, - 'order_book_min': {'type': 'integer', 'minimum': 1}, - 'order_book_max': {'type': 'integer', 'minimum': 1, 'maximum': 50}, + 'order_book_top': {'type': 'integer', 'minimum': 1, 'maximum': 50, }, 'use_sell_signal': {'type': 'boolean'}, 'sell_profit_only': {'type': 'boolean'}, 'sell_profit_offset': {'type': 'number'}, 'ignore_roi_if_buy_signal': {'type': 'boolean'} - } + }, + 'required': ['price_side'] }, 'order_types': { 'type': 'object', diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 99430ea25..0315418bf 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -999,15 +999,6 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e - def _order_book_gen(self, pair: str, side: str, order_book_max: int = 1, - order_book_min: int = 1): - """ - Helper generator to query orderbook in loop (used for early sell-order placing) - """ - order_book = self.fetch_l2_order_book(pair, order_book_max) - for i in range(order_book_min, order_book_max + 1): - yield order_book[side][i - 1][0] - def get_buy_rate(self, pair: str, refresh: bool) -> float: """ Calculates bid target between current ask price and last price @@ -1076,10 +1067,15 @@ class Exchange: logger.info( f"Getting price from order book {ask_strategy['price_side'].capitalize()} side." ) + order_book_top = ask_strategy.get('order_book_top', 1) + order_book = self.fetch_l2_order_book(pair, order_book_top) try: - rate = next(self._order_book_gen(pair, f"{ask_strategy['price_side']}s")) + rate = order_book[f"{ask_strategy['price_side']}s"][order_book_top - 1][0] except (IndexError, KeyError) as e: - logger.warning("Sell Price at location from orderbook could not be determined.") + logger.warning( + f"Sell Price at location {order_book_top} from orderbook could not be " + f"determined. Orderbook: {order_book}" + ) raise PricingError from e else: ticker = self.fetch_ticker(pair) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e533e15e0..d03604cb0 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -693,37 +693,10 @@ class FreqtradeBot(LoggingMixin): (buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.timeframe, analyzed_df) - if config_ask_strategy.get('use_order_book', False): - order_book_min = config_ask_strategy.get('order_book_min', 1) - order_book_max = config_ask_strategy.get('order_book_max', 1) - logger.debug(f'Using order book between {order_book_min} and {order_book_max} ' - f'for selling {trade.pair}...') - - order_book = self.exchange._order_book_gen( - trade.pair, f"{config_ask_strategy['price_side']}s", - order_book_min=order_book_min, order_book_max=order_book_max) - for i in range(order_book_min, order_book_max + 1): - try: - sell_rate = next(order_book) - except (IndexError, KeyError) as e: - logger.warning( - f"Sell Price at location {i} from orderbook could not be determined." - ) - raise PricingError from e - logger.debug(f" order book {config_ask_strategy['price_side']} top {i}: " - f"{sell_rate:0.8f}") - # Assign sell-rate to cache - otherwise sell-rate is never updated in the cache, - # resulting in outdated RPC messages - self.exchange._sell_rate_cache[trade.pair] = sell_rate - - if self._check_and_execute_sell(trade, sell_rate, buy, sell): - return True - - else: - logger.debug('checking sell') - sell_rate = self.exchange.get_sell_rate(trade.pair, True) - if self._check_and_execute_sell(trade, sell_rate, buy, sell): - return True + logger.debug('checking sell') + sell_rate = self.exchange.get_sell_rate(trade.pair, True) + if self._check_and_execute_sell(trade, sell_rate, buy, sell): + return True logger.debug('Found no sell signal for %s.', trade) return False diff --git a/freqtrade/templates/base_config.json.j2 b/freqtrade/templates/base_config.json.j2 index 8933ebc6a..043d95bdd 100644 --- a/freqtrade/templates/base_config.json.j2 +++ b/freqtrade/templates/base_config.json.j2 @@ -15,7 +15,7 @@ "bid_strategy": { "price_side": "bid", "ask_last_balance": 0.0, - "use_order_book": false, + "use_order_book": true, "order_book_top": 1, "check_depth_of_market": { "enabled": false, @@ -24,9 +24,8 @@ }, "ask_strategy": { "price_side": "ask", - "use_order_book": false, - "order_book_min": 1, - "order_book_max": 1, + "use_order_book": true, + "order_book_top": 1, "use_sell_signal": true, "sell_profit_only": false, "ignore_roi_if_buy_signal": false diff --git a/tests/conftest.py b/tests/conftest.py index 6e00db274..a843d9397 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -290,8 +290,7 @@ def get_default_conf(testdatadir): }, "ask_strategy": { "use_order_book": False, - "order_book_min": 1, - "order_book_max": 1 + "order_book_top": 1, }, "exchange": { "name": "binance", diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index ba9ec8bc7..524dc873c 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1844,8 +1844,7 @@ def test_get_sell_rate_orderbook(default_conf, mocker, caplog, side, expected, o # Test orderbook mode default_conf['ask_strategy']['price_side'] = side default_conf['ask_strategy']['use_order_book'] = True - default_conf['ask_strategy']['order_book_min'] = 1 - default_conf['ask_strategy']['order_book_max'] = 2 + default_conf['ask_strategy']['order_book_top'] = 1 pair = "ETH/BTC" mocker.patch('freqtrade.exchange.Exchange.fetch_l2_order_book', order_book_l2) exchange = get_patched_exchange(mocker, default_conf) @@ -1862,8 +1861,7 @@ def test_get_sell_rate_orderbook_exception(default_conf, mocker, caplog): # Test orderbook mode default_conf['ask_strategy']['price_side'] = 'ask' default_conf['ask_strategy']['use_order_book'] = True - default_conf['ask_strategy']['order_book_min'] = 1 - default_conf['ask_strategy']['order_book_max'] = 2 + default_conf['ask_strategy']['order_book_top'] = 1 pair = "ETH/BTC" # Test What happens if the exchange returns an empty orderbook. mocker.patch('freqtrade.exchange.Exchange.fetch_l2_order_book', @@ -1871,7 +1869,8 @@ def test_get_sell_rate_orderbook_exception(default_conf, mocker, caplog): exchange = get_patched_exchange(mocker, default_conf) with pytest.raises(PricingError): exchange.get_sell_rate(pair, True) - assert log_has("Sell Price at location from orderbook could not be determined.", caplog) + assert log_has_re(r"Sell Price at location 1 from orderbook could not be determined\..*", + caplog) def test_get_sell_rate_exception(default_conf, mocker, caplog): diff --git a/tests/test_configuration.py b/tests/test_configuration.py index c5d0cd908..366501d8a 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -935,6 +935,23 @@ def test_validate_protections(default_conf, protconf, expected): validate_config_consistency(conf) +def test_validate_ask_orderbook(default_conf, caplog) -> None: + conf = deepcopy(default_conf) + conf['ask_strategy']['use_order_book'] = True + conf['ask_strategy']['order_book_min'] = 2 + conf['ask_strategy']['order_book_max'] = 2 + + validate_config_consistency(conf) + assert log_has_re(r"DEPRECATED: Please use `order_book_top` instead of.*", caplog) + assert conf['ask_strategy']['order_book_top'] == 2 + + conf['ask_strategy']['order_book_max'] = 5 + + with pytest.raises(OperationalException, + match=r"Using order_book_max != order_book_min in ask_strategy.*"): + 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 aac8d0c4c..4f8a18827 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -3990,8 +3990,7 @@ def test_order_book_ask_strategy(default_conf, limit_buy_order_open, limit_buy_o mocker.patch('freqtrade.exchange.Exchange.fetch_l2_order_book', order_book_l2) default_conf['exchange']['name'] = 'binance' default_conf['ask_strategy']['use_order_book'] = True - default_conf['ask_strategy']['order_book_min'] = 1 - default_conf['ask_strategy']['order_book_max'] = 2 + default_conf['ask_strategy']['order_book_top'] = 1 default_conf['telegram']['enabled'] = False patch_RPCManager(mocker) patch_exchange(mocker) @@ -4027,7 +4026,8 @@ def test_order_book_ask_strategy(default_conf, limit_buy_order_open, limit_buy_o return_value={'bids': [[]], 'asks': [[]]}) with pytest.raises(PricingError): freqtrade.handle_trade(trade) - assert log_has('Sell Price at location 1 from orderbook could not be determined.', caplog) + assert log_has_re(r'Sell Price at location 1 from orderbook could not be determined\..*', + caplog) def test_startup_state(default_conf, mocker):