Convert limit orders to market orders if they cross a threshold

closes #7786
This commit is contained in:
Matthias 2023-02-16 20:20:06 +01:00
parent 9600039686
commit 22700527ac
2 changed files with 31 additions and 13 deletions

View File

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

View File

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