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]: | ||||
|         order_id = f'dry_run_{side}_{datetime.now().timestamp()}' | ||||
|         _amount = self.amount_to_precision(pair, amount) | ||||
|         dry_order = { | ||||
|         dry_order: Dict[str, Any] = { | ||||
|             'id': order_id, | ||||
|             'symbol': pair, | ||||
|             'price': rate, | ||||
| @@ -577,26 +577,73 @@ class Exchange: | ||||
|             'fee': None, | ||||
|             '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 | ||||
|         return dry_order | ||||
|  | ||||
|     def _store_dry_order(self, dry_order: Dict, pair: str) -> None: | ||||
|         closed_order = dry_order.copy() | ||||
|         if closed_order['type'] in ["market", "limit"]: | ||||
|             closed_order.update({ | ||||
|                 'status': 'closed', | ||||
|                 'filled': closed_order['amount'], | ||||
|                 'remaining': 0, | ||||
|     def add_dry_order_fee(self, pair: str, dry_order: Dict[str, Any]): | ||||
|         dry_order.update({ | ||||
|             'fee': { | ||||
|                 'currency': self.get_pair_quote_currency(pair), | ||||
|                 'cost': dry_order['cost'] * self.get_fee(pair), | ||||
|                 'rate': self.get_fee(pair) | ||||
|             } | ||||
|         }) | ||||
|         if closed_order["type"] in ["stop_loss_limit", "stop-loss-limit"]: | ||||
|             closed_order["info"].update({"stopPrice": closed_order["price"]}) | ||||
|         self._dry_run_open_orders[closed_order["id"]] = closed_order | ||||
|  | ||||
|     def get_dry_market_fill_price(self, pair: str, side: str, amount: float, rate: float) -> float: | ||||
|         """ | ||||
|         Get the market order fill price based on orderbook interpolation | ||||
|         """ | ||||
|         if self.exchange_has('fetchL2OrderBook'): | ||||
|             ob = self.fetch_l2_order_book(pair, 20) | ||||
|             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]: | ||||
|         """ | ||||
| @@ -605,6 +652,16 @@ class Exchange: | ||||
|         """ | ||||
|         try: | ||||
|             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 | ||||
|         except KeyError as e: | ||||
|             # 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): | ||||
|     default_conf['dry_run'] = True | ||||
|     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_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), | ||||
|         fetch_ticker=ticker, | ||||
|         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)) | ||||
|  | ||||
|   | ||||
| @@ -225,6 +225,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: | ||||
|         'freqtrade.exchange.Exchange', | ||||
|         fetch_ticker=ticker, | ||||
|         get_fee=fee, | ||||
|         dry_limit_order_filled=MagicMock(return_value=True), | ||||
|     ) | ||||
|     status_table = MagicMock() | ||||
|     mocker.patch.multiple( | ||||
| @@ -671,6 +672,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee, | ||||
|         'freqtrade.exchange.Exchange', | ||||
|         fetch_ticker=ticker, | ||||
|         get_fee=fee, | ||||
|         dry_limit_order_filled=MagicMock(return_value=True), | ||||
|     ) | ||||
|  | ||||
|     freqtradebot = FreqtradeBot(default_conf) | ||||
| @@ -729,6 +731,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, | ||||
|         'freqtrade.exchange.Exchange', | ||||
|         fetch_ticker=ticker, | ||||
|         get_fee=fee, | ||||
|         dry_limit_order_filled=MagicMock(return_value=True), | ||||
|     ) | ||||
|  | ||||
|     freqtradebot = FreqtradeBot(default_conf) | ||||
| @@ -789,6 +792,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None | ||||
|         'freqtrade.exchange.Exchange', | ||||
|         fetch_ticker=ticker, | ||||
|         get_fee=fee, | ||||
|         dry_limit_order_filled=MagicMock(return_value=True), | ||||
|     ) | ||||
|     default_conf['max_open_trades'] = 4 | ||||
|     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, | ||||
|         stoploss=stoploss, | ||||
|         cancel_stoploss_order=cancel_order, | ||||
|         dry_limit_order_filled=MagicMock(return_value=True), | ||||
|     ) | ||||
|  | ||||
|     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, | ||||
|         amount_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={ | ||||
|   | ||||
		Reference in New Issue
	
	Block a user