Update dry-run order handling to use realistic fill prices
closes #3389
This commit is contained in:
		| @@ -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. | ||||||
|   | |||||||
| @@ -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') == {} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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)) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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) | ||||||
|   | |||||||
| @@ -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={ | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user