diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 67676d4e0..87798e612 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -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, - '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 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) + } + }) + + 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. diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 5fa94e6c1..73b8022d1 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -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') == {} diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index def2e43c6..f47819568 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -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)) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 4c60bdad3..50c8a36ce 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -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) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 66866a8fc..0866deead 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -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={