From 22700527ac768d6924fbe0e2277272a6d363449f Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 16 Feb 2023 20:20:06 +0100 Subject: [PATCH] Convert limit orders to market orders if they cross a threshold closes #7786 --- freqtrade/exchange/exchange.py | 17 +++++++++++++---- tests/exchange/test_exchange.py | 27 ++++++++++++++++++--------- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index c1b24b461..9b7cfc5c9 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -850,7 +850,7 @@ class Exchange: 'remaining': _amount, 'datetime': arrow.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ'), 'timestamp': arrow.utcnow().int_timestamp * 1000, - 'status': "closed" if ordertype == "market" and not stop_loss else "open", + 'status': "open", 'fee': None, 'info': {}, 'leverage': leverage @@ -863,6 +863,14 @@ class Exchange: orderbook: Optional[OrderBook] = None if self.exchange_has('fetchL2OrderBook'): orderbook = self.fetch_l2_order_book(pair, 20) + if ordertype == "limit" and orderbook: + # Allow a 3% price difference + allowed_diff = 0.03 + if self._dry_is_price_crossed(pair, side, rate, orderbook, allowed_diff): + logger.info( + f"Converted order {pair} to market order due to price {rate} crossing spread " + f"by more than {allowed_diff:.2%}.") + dry_order["type"] = "market" if dry_order["type"] == "market" and not dry_order.get("ft_order_type"): # Update market order pricing @@ -871,6 +879,7 @@ class Exchange: 'average': average, 'filled': _amount, 'remaining': 0.0, + 'status': "closed", 'cost': (dry_order['amount'] * average) / leverage }) # market orders will always incurr taker fees @@ -943,7 +952,7 @@ class Exchange: return rate def _dry_is_price_crossed(self, pair: str, side: str, limit: float, - orderbook: Optional[OrderBook] = None) -> bool: + orderbook: Optional[OrderBook] = None, offset: float = 0.0) -> bool: if not self.exchange_has('fetchL2OrderBook'): return True if not orderbook: @@ -951,11 +960,11 @@ class Exchange: try: if side == 'buy': price = orderbook['asks'][0][0] - if limit >= price: + if limit * (1 - offset) >= price: return True else: price = orderbook['bids'][0][0] - if limit <= price: + if limit * (1 + offset) <= price: return True except IndexError: # Ignore empty orderbooks when filling - can be filled with the next iteration. diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 01a5060bb..13613df37 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1248,18 +1248,20 @@ def test_create_dry_run_order_fees( assert order1['fee']['rate'] == fee -@pytest.mark.parametrize("side,price,filled", [ +@pytest.mark.parametrize("side,price,filled,converted", [ # order_book_l2_usd spread: # best ask: 25.566 # best bid: 25.563 - ("buy", 25.563, False), - ("buy", 25.566, True), - ("sell", 25.566, False), - ("sell", 25.563, True), + ("buy", 25.563, False, False), + ("buy", 25.566, True, False), + ("sell", 25.566, False, False), + ("sell", 25.563, True, False), + ("buy", 29.563, True, True), + ("sell", 21.563, True, True), ]) @pytest.mark.parametrize("exchange_name", EXCHANGES) -def test_create_dry_run_order_limit_fill(default_conf, mocker, side, price, filled, - exchange_name, order_book_l2_usd): +def test_create_dry_run_order_limit_fill(default_conf, mocker, side, price, filled, caplog, + exchange_name, order_book_l2_usd, converted): default_conf['dry_run'] = True exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) mocker.patch.multiple('freqtrade.exchange.Exchange', @@ -1279,9 +1281,16 @@ def test_create_dry_run_order_limit_fill(default_conf, mocker, side, price, fill assert 'id' in order assert f'dry_run_{side}_' in order["id"] assert order["side"] == side - assert order["type"] == "limit" + if not converted: + assert order["average"] == price + assert order["type"] == "limit" + else: + # Converted to market order + assert order["type"] == "market" + assert 25.5 < order["average"] < 25.6 + assert log_has_re(r"Converted .* to market order.*", caplog) + assert order["symbol"] == "LTC/USDT" - assert order["average"] == price assert order['status'] == 'open' if not filled else 'closed' order_book_l2_usd.reset_mock()