Update dry-run order handling to use realistic fill prices

closes #3389
This commit is contained in:
Matthias 2021-06-03 20:55:18 +02:00
parent a0893b291a
commit 1e988c97ad
5 changed files with 84 additions and 19 deletions

View File

@ -561,7 +561,7 @@ class Exchange:
rate: float, params: Dict = {}) -> Dict[str, Any]: rate: float, params: Dict = {}) -> Dict[str, Any]:
order_id = f'dry_run_{side}_{datetime.now().timestamp()}' order_id = f'dry_run_{side}_{datetime.now().timestamp()}'
_amount = self.amount_to_precision(pair, amount) _amount = self.amount_to_precision(pair, amount)
dry_order = { dry_order: Dict[str, Any] = {
'id': order_id, 'id': order_id,
'symbol': pair, 'symbol': pair,
'price': rate, 'price': rate,
@ -577,26 +577,73 @@ class Exchange:
'fee': None, 'fee': None,
'info': {} 'info': {}
} }
self._store_dry_order(dry_order, pair) if dry_order["type"] in ["stop_loss_limit", "stop-loss-limit"]:
dry_order["info"] = {"stopPrice": dry_order["price"]}
if dry_order["type"] == "market":
# Update market order pricing
average = self.get_dry_market_fill_price(pair, side, amount, rate)
dry_order.update({
'average': average,
'cost': dry_order['amount'] * average,
})
self.add_dry_order_fee(pair, dry_order)
self._dry_run_open_orders[dry_order["id"]] = dry_order
# Copy order and close it - so the returned order is open unless it's a market order # Copy order and close it - so the returned order is open unless it's a market order
return dry_order return dry_order
def _store_dry_order(self, dry_order: Dict, pair: str) -> None: def add_dry_order_fee(self, pair: str, dry_order: Dict[str, Any]):
closed_order = dry_order.copy() dry_order.update({
if closed_order['type'] in ["market", "limit"]: 'fee': {
closed_order.update({ 'currency': self.get_pair_quote_currency(pair),
'status': 'closed', 'cost': dry_order['cost'] * self.get_fee(pair),
'filled': closed_order['amount'], 'rate': self.get_fee(pair)
'remaining': 0, }
'fee': { })
'currency': self.get_pair_quote_currency(pair),
'cost': dry_order['cost'] * self.get_fee(pair), def get_dry_market_fill_price(self, pair: str, side: str, amount: float, rate: float) -> float:
'rate': self.get_fee(pair) """
} Get the market order fill price based on orderbook interpolation
}) """
if closed_order["type"] in ["stop_loss_limit", "stop-loss-limit"]: if self.exchange_has('fetchL2OrderBook'):
closed_order["info"].update({"stopPrice": closed_order["price"]}) ob = self.fetch_l2_order_book(pair, 20)
self._dry_run_open_orders[closed_order["id"]] = closed_order book_entry_type = 'asks' if side == 'buy' else 'bids'
remaining_amount = amount
filled_amount = 0
for book_entry in ob[book_entry_type]:
book_entry_price = book_entry[0]
book_entry_coin_volume = book_entry[1]
book_entry_ref_currency_volume = book_entry_price * book_entry_coin_volume
if remaining_amount > 0:
if remaining_amount < book_entry_ref_currency_volume:
filled_amount += remaining_amount * book_entry_price
else:
filled_amount += book_entry_ref_currency_volume * book_entry_price
remaining_amount -= book_entry_ref_currency_volume
else:
break
forecast_avg_filled_price = filled_amount / amount
return self.price_to_precision(pair, forecast_avg_filled_price)
return rate
def dry_limit_order_filled(self, pair: str, side: str, limit: float) -> bool:
if not self.exchange_has('fetchL2OrderBook'):
return True
ob = self.fetch_l2_order_book(pair, 1)
if side == 'buy':
price = ob['asks'][0][0]
logger.debug(f"{pair} checking dry buy-order: price={price}, limit={limit}")
if limit >= price:
return True
else:
price = ob['bids'][0][0]
logger.debug(f"{pair} checking dry sell-order: price={price}, limit={limit}")
if limit <= price:
return True
return False
def fetch_dry_run_order(self, order_id) -> Dict[str, Any]: def fetch_dry_run_order(self, order_id) -> Dict[str, Any]:
""" """
@ -605,6 +652,16 @@ class Exchange:
""" """
try: try:
order = self._dry_run_open_orders[order_id] order = self._dry_run_open_orders[order_id]
pair = order['symbol']
if order['status'] != "closed" and order['type'] in ["limit"]:
if self.dry_limit_order_filled(pair, order['side'], order['price']):
order.update({
'status': 'closed',
'filled': order['amount'],
'remaining': 0,
})
self.add_dry_order_fee(pair, order)
return order return order
except KeyError as e: except KeyError as e:
# Gracefully handle errors with dry-run orders. # Gracefully handle errors with dry-run orders.

View File

@ -2117,6 +2117,7 @@ def test_get_historic_trades_notsupported(default_conf, mocker, caplog, exchange
def test_cancel_order_dry_run(default_conf, mocker, exchange_name): def test_cancel_order_dry_run(default_conf, mocker, exchange_name):
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('freqtrade.exchange.Exchange.dry_limit_order_filled', return_value=True)
assert exchange.cancel_order(order_id='123', pair='TKN/BTC') == {} assert exchange.cancel_order(order_id='123', pair='TKN/BTC') == {}
assert exchange.cancel_stoploss_order(order_id='123', pair='TKN/BTC') == {} assert exchange.cancel_stoploss_order(order_id='123', pair='TKN/BTC') == {}

View File

@ -996,7 +996,8 @@ def test_api_forcesell(botclient, mocker, ticker, fee, markets):
get_balances=MagicMock(return_value=ticker), get_balances=MagicMock(return_value=ticker),
fetch_ticker=ticker, fetch_ticker=ticker,
get_fee=fee, get_fee=fee,
markets=PropertyMock(return_value=markets) markets=PropertyMock(return_value=markets),
dry_limit_order_filled=MagicMock(return_value=True),
) )
patch_get_signal(ftbot, (True, False)) patch_get_signal(ftbot, (True, False))

View File

@ -225,6 +225,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None:
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=ticker, fetch_ticker=ticker,
get_fee=fee, get_fee=fee,
dry_limit_order_filled=MagicMock(return_value=True),
) )
status_table = MagicMock() status_table = MagicMock()
mocker.patch.multiple( mocker.patch.multiple(
@ -671,6 +672,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee,
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=ticker, fetch_ticker=ticker,
get_fee=fee, get_fee=fee,
dry_limit_order_filled=MagicMock(return_value=True),
) )
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
@ -729,6 +731,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee,
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=ticker, fetch_ticker=ticker,
get_fee=fee, get_fee=fee,
dry_limit_order_filled=MagicMock(return_value=True),
) )
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
@ -789,6 +792,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=ticker, fetch_ticker=ticker,
get_fee=fee, get_fee=fee,
dry_limit_order_filled=MagicMock(return_value=True),
) )
default_conf['max_open_trades'] = 4 default_conf['max_open_trades'] = 4
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)

View File

@ -2750,6 +2750,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke
price_to_precision=lambda s, x, y: y, price_to_precision=lambda s, x, y: y,
stoploss=stoploss, stoploss=stoploss,
cancel_stoploss_order=cancel_order, cancel_stoploss_order=cancel_order,
dry_limit_order_filled=MagicMock(return_value=True),
) )
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
@ -2792,6 +2793,7 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f
get_fee=fee, get_fee=fee,
amount_to_precision=lambda s, x, y: y, amount_to_precision=lambda s, x, y: y,
price_to_precision=lambda s, x, y: y, price_to_precision=lambda s, x, y: y,
dry_limit_order_filled=MagicMock(return_value=True),
) )
stoploss = MagicMock(return_value={ stoploss = MagicMock(return_value={