Convert limit orders to market orders if they cross a threshold
closes #7786
This commit is contained in:
parent
9600039686
commit
22700527ac
@ -850,7 +850,7 @@ class Exchange:
|
|||||||
'remaining': _amount,
|
'remaining': _amount,
|
||||||
'datetime': arrow.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
|
'datetime': arrow.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
|
||||||
'timestamp': arrow.utcnow().int_timestamp * 1000,
|
'timestamp': arrow.utcnow().int_timestamp * 1000,
|
||||||
'status': "closed" if ordertype == "market" and not stop_loss else "open",
|
'status': "open",
|
||||||
'fee': None,
|
'fee': None,
|
||||||
'info': {},
|
'info': {},
|
||||||
'leverage': leverage
|
'leverage': leverage
|
||||||
@ -863,6 +863,14 @@ class Exchange:
|
|||||||
orderbook: Optional[OrderBook] = None
|
orderbook: Optional[OrderBook] = None
|
||||||
if self.exchange_has('fetchL2OrderBook'):
|
if self.exchange_has('fetchL2OrderBook'):
|
||||||
orderbook = self.fetch_l2_order_book(pair, 20)
|
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"):
|
if dry_order["type"] == "market" and not dry_order.get("ft_order_type"):
|
||||||
# Update market order pricing
|
# Update market order pricing
|
||||||
@ -871,6 +879,7 @@ class Exchange:
|
|||||||
'average': average,
|
'average': average,
|
||||||
'filled': _amount,
|
'filled': _amount,
|
||||||
'remaining': 0.0,
|
'remaining': 0.0,
|
||||||
|
'status': "closed",
|
||||||
'cost': (dry_order['amount'] * average) / leverage
|
'cost': (dry_order['amount'] * average) / leverage
|
||||||
})
|
})
|
||||||
# market orders will always incurr taker fees
|
# market orders will always incurr taker fees
|
||||||
@ -943,7 +952,7 @@ class Exchange:
|
|||||||
return rate
|
return rate
|
||||||
|
|
||||||
def _dry_is_price_crossed(self, pair: str, side: str, limit: float,
|
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'):
|
if not self.exchange_has('fetchL2OrderBook'):
|
||||||
return True
|
return True
|
||||||
if not orderbook:
|
if not orderbook:
|
||||||
@ -951,11 +960,11 @@ class Exchange:
|
|||||||
try:
|
try:
|
||||||
if side == 'buy':
|
if side == 'buy':
|
||||||
price = orderbook['asks'][0][0]
|
price = orderbook['asks'][0][0]
|
||||||
if limit >= price:
|
if limit * (1 - offset) >= price:
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
price = orderbook['bids'][0][0]
|
price = orderbook['bids'][0][0]
|
||||||
if limit <= price:
|
if limit * (1 + offset) <= price:
|
||||||
return True
|
return True
|
||||||
except IndexError:
|
except IndexError:
|
||||||
# Ignore empty orderbooks when filling - can be filled with the next iteration.
|
# Ignore empty orderbooks when filling - can be filled with the next iteration.
|
||||||
|
@ -1248,18 +1248,20 @@ def test_create_dry_run_order_fees(
|
|||||||
assert order1['fee']['rate'] == fee
|
assert order1['fee']['rate'] == fee
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("side,price,filled", [
|
@pytest.mark.parametrize("side,price,filled,converted", [
|
||||||
# order_book_l2_usd spread:
|
# order_book_l2_usd spread:
|
||||||
# best ask: 25.566
|
# best ask: 25.566
|
||||||
# best bid: 25.563
|
# best bid: 25.563
|
||||||
("buy", 25.563, False),
|
("buy", 25.563, False, False),
|
||||||
("buy", 25.566, True),
|
("buy", 25.566, True, False),
|
||||||
("sell", 25.566, False),
|
("sell", 25.566, False, False),
|
||||||
("sell", 25.563, True),
|
("sell", 25.563, True, False),
|
||||||
|
("buy", 29.563, True, True),
|
||||||
|
("sell", 21.563, True, True),
|
||||||
])
|
])
|
||||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||||
def test_create_dry_run_order_limit_fill(default_conf, mocker, side, price, filled,
|
def test_create_dry_run_order_limit_fill(default_conf, mocker, side, price, filled, caplog,
|
||||||
exchange_name, order_book_l2_usd):
|
exchange_name, order_book_l2_usd, converted):
|
||||||
default_conf['dry_run'] = True
|
default_conf['dry_run'] = True
|
||||||
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
||||||
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
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 'id' in order
|
||||||
assert f'dry_run_{side}_' in order["id"]
|
assert f'dry_run_{side}_' in order["id"]
|
||||||
assert order["side"] == side
|
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["symbol"] == "LTC/USDT"
|
||||||
assert order["average"] == price
|
|
||||||
assert order['status'] == 'open' if not filled else 'closed'
|
assert order['status'] == 'open' if not filled else 'closed'
|
||||||
order_book_l2_usd.reset_mock()
|
order_book_l2_usd.reset_mock()
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user