Repository: freqtrade/freqtrade

Issue:
Take a 'realist' filled price in dry run for market orders (compute using order book and the stake amount)
## Describe the enhancement

### Story like :

As a trader
I want the dry run to be closed to real money behavior
So that i can make a choice on number of asset i trade with (depends liquidity).

### Further explanations:

What about compute the average filled price (as it is a market buy and market sell) using the order book and taking into account the stake_amount ?

Then this average filled price is used to compute profit.

### The snippet :

```
def forecast_avg_filled_price(self, action, stake_amount, orderbook) -> float:
        
        book_entry_type = None
        if action == 'buy':
            book_entry_type = 'asks'
        if action == 'sell':
            book_entry_type = 'bids'
        
        remain_amount_to_be_filled = stake_amount
        filled_amount = 0
        for book_entry in orderbook[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 remain_amount_to_be_filled > 0:
                if remain_amount_to_be_filled < book_entry_ref_currency_volume:
                    filled_amount += remain_amount_to_be_filled * book_entry_price
                else:
                    filled_amount += book_entry_ref_currency_volume * book_entry_price
                remain_amount_to_be_filled -= book_entry_ref_currency_volume
                # print(' {} : book_entry price : {} with ref volume {}'.format(action, book_entry_price, book_entry_ref_currency_volume))
                # print(' {} : remain_amount_to_be_filled {} '.format(action, remain_amount_to_be_filled))
            else:
                break
        forecast_avg_filled_price = filled_amount / stake_amount
        return forecast_avg_filled_price
```
Have a nice day.





Discussion:
but this would only apply on market orders?
for limit you cannot calculate this this way - as limit orders don't have this kind of slippage.

it's not a bad idea for market orders (which are currently not well emulated) - but for limit orders it's more difficult i think - these never show slippage - so the situation is quite different.
to emulate limit orders you would have to fetch past trades since sell reason triggered and match price/amounts. This is of course unfeasable for very liquid markets, at the same time it is not very useful in those cases, since trades are easily filled and spread is low, so it could be enabled on case by case if the pair is below some threshold of volume/trades frequency. 

Resolving patch:
diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py
--- a/freqtrade/exchange/exchange.py
+++ b/freqtrade/exchange/exchange.py
@@ -561,7 +561,7 @@ def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: flo
                              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,94 @@ def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: flo
             '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,
+            })
+            dry_order = self.add_dry_order_fee(pair, dry_order)
+
+        dry_order = self.check_dry_limit_order_filled(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]) -> 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)
+            }
+        })
+        return dry_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)
+            ob_type = 'asks' if side == 'buy' else 'bids'
+
+            remaining_amount = amount
+            filled_amount = 0
+            for book_entry in ob[ob_type]:
+                book_entry_price = book_entry[0]
+                book_entry_coin_volume = book_entry[1]
+                if remaining_amount > 0:
+                    if remaining_amount < book_entry_coin_volume:
+                        filled_amount += remaining_amount * book_entry_price
+                    else:
+                        filled_amount += book_entry_coin_volume * book_entry_price
+                    remaining_amount -= book_entry_coin_volume
+                else:
+                    break
+            else:
+                # If remaining_amount wasn't consumed completely (break was not called)
+                filled_amount += remaining_amount * book_entry_price
+            forecast_avg_filled_price = filled_amount / amount
+            return self.price_to_precision(pair, forecast_avg_filled_price)
+
+        return rate
+
+    def _is_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 check_dry_limit_order_filled(self, order: Dict[str, Any]) -> Dict[str, Any]:
+        """
+        Check dry-run limit order fill and update fee (if it filled).
+        """
+        if order['status'] != "closed" and order['type'] in ["limit"]:
+            pair = order['symbol']
+            if self._is_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
 
     def fetch_dry_run_order(self, order_id) -> Dict[str, Any]:
         """
@@ -605,6 +673,7 @@ def fetch_dry_run_order(self, order_id) -> Dict[str, Any]:
         """
         try:
             order = self._dry_run_open_orders[order_id]
+            order = self.check_dry_limit_order_filled(order)
             return order
         except KeyError as e:
             # Gracefully handle errors with dry-run orders.


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1087,6 +1087,40 @@ def order_book_l2():
     })
 
 
+@pytest.fixture
+def order_book_l2_usd():
+    return MagicMock(return_value={
+        'symbol': 'LTC/USDT',
+        'bids': [
+            [25.563, 49.269],
+            [25.562, 83.0],
+            [25.56, 106.0],
+            [25.559, 15.381],
+            [25.558, 29.299],
+            [25.557, 34.624],
+            [25.556, 10.0],
+            [25.555, 14.684],
+            [25.554, 45.91],
+            [25.553, 50.0]
+        ],
+        'asks': [
+            [25.566, 14.27],
+            [25.567, 48.484],
+            [25.568, 92.349],
+            [25.572, 31.48],
+            [25.573, 23.0],
+            [25.574, 20.0],
+            [25.575, 89.606],
+            [25.576, 262.016],
+            [25.577, 178.557],
+            [25.578, 78.614]
+            ],
+        'timestamp': None,
+        'datetime': None,
+        'nonce': 2372149736
+    })
+
+
 @pytest.fixture
 def ohlcv_history_list():
     return [
diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py
--- a/tests/exchange/test_exchange.py
+++ b/tests/exchange/test_exchange.py
@@ -947,6 +947,72 @@ def test_create_dry_run_order(default_conf, mocker, side, exchange_name):
     assert order["symbol"] == "ETH/BTC"
 
 
+@pytest.mark.parametrize("side,startprice,endprice", [
+    ("buy", 25.563, 25.566),
+    ("sell", 25.566, 25.563)
+])
+@pytest.mark.parametrize("exchange_name", EXCHANGES)
+def test_create_dry_run_order_limit_fill(default_conf, mocker, side, startprice, endprice,
+                                         exchange_name, order_book_l2_usd):
+    default_conf['dry_run'] = True
+    exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
+    mocker.patch.multiple('freqtrade.exchange.Exchange',
+                          exchange_has=MagicMock(return_value=True),
+                          fetch_l2_order_book=order_book_l2_usd,
+                          )
+
+    order = exchange.create_dry_run_order(
+        pair='LTC/USDT', ordertype='limit', side=side, amount=1, rate=startprice)
+    assert order_book_l2_usd.call_count == 1
+    assert 'id' in order
+    assert f'dry_run_{side}_' in order["id"]
+    assert order["side"] == side
+    assert order["type"] == "limit"
+    assert order["symbol"] == "LTC/USDT"
+    order_book_l2_usd.reset_mock()
+
+    order_closed = exchange.fetch_dry_run_order(order['id'])
+    assert order_book_l2_usd.call_count == 1
+    assert order_closed['status'] == 'open'
+    assert not order['fee']
+
+    order_book_l2_usd.reset_mock()
+    order_closed['price'] = endprice
+
+    order_closed = exchange.fetch_dry_run_order(order['id'])
+    assert order_closed['status'] == 'closed'
+    assert order['fee']
+
+
+@pytest.mark.parametrize("side,amount,endprice", [
+    ("buy", 1, 25.566),
+    ("buy", 100, 25.5672),  # Requires interpolation
+    ("buy", 1000, 25.575),  # More than orderbook return
+    ("sell", 1, 25.563),
+    ("sell", 100, 25.5625),  # Requires interpolation
+    ("sell", 1000, 25.5555),  # More than orderbook return
+])
+@pytest.mark.parametrize("exchange_name", EXCHANGES)
+def test_create_dry_run_order_market_fill(default_conf, mocker, side, amount, endprice,
+                                          exchange_name, order_book_l2_usd):
+    default_conf['dry_run'] = True
+    exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
+    mocker.patch.multiple('freqtrade.exchange.Exchange',
+                          exchange_has=MagicMock(return_value=True),
+                          fetch_l2_order_book=order_book_l2_usd,
+                          )
+
+    order = exchange.create_dry_run_order(
+        pair='LTC/USDT', ordertype='market', side=side, amount=amount, rate=25.5)
+    assert 'id' in order
+    assert f'dry_run_{side}_' in order["id"]
+    assert order["side"] == side
+    assert order["type"] == "market"
+    assert order["symbol"] == "LTC/USDT"
+    assert order['status'] == 'closed'
+    assert round(order["average"], 4) == round(endprice, 4)
+
+
 @pytest.mark.parametrize("side", [
     ("buy"),
     ("sell")
@@ -2117,6 +2183,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._is_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.py b/tests/rpc/test_rpc.py
--- a/tests/rpc/test_rpc.py
+++ b/tests/rpc/test_rpc.py
@@ -679,6 +679,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
                 'filled': 0.0,
             }
         ),
+        _is_dry_limit_order_filled=MagicMock(return_value=True),
         get_fee=fee,
     )
     mocker.patch('freqtrade.wallets.Wallets.get_free', return_value=1000)
@@ -703,8 +704,8 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
     assert msg == {'result': 'Created sell orders for all open trades.'}
 
     freqtradebot.enter_positions()
-    msg = rpc._rpc_forcesell('1')
-    assert msg == {'result': 'Created sell order for trade 1.'}
+    msg = rpc._rpc_forcesell('2')
+    assert msg == {'result': 'Created sell order for trade 2.'}
 
     freqtradebot.state = State.STOPPED
     with pytest.raises(RPCException, match=r'.*trader is not running*'):
@@ -715,9 +716,11 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
 
     freqtradebot.state = State.RUNNING
     assert cancel_order_mock.call_count == 0
+    mocker.patch(
+        'freqtrade.exchange.Exchange._is_dry_limit_order_filled', MagicMock(return_value=False))
     freqtradebot.enter_positions()
     # make an limit-buy open trade
-    trade = Trade.query.filter(Trade.id == '1').first()
+    trade = Trade.query.filter(Trade.id == '3').first()
     filled_amount = trade.amount / 2
     # Fetch order - it's open first, and closed after cancel_order is called.
     mocker.patch(
@@ -738,7 +741,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
     )
     # check that the trade is called, which is done by ensuring exchange.cancel_order is called
     # and trade amount is updated
-    rpc._rpc_forcesell('1')
+    rpc._rpc_forcesell('3')
     assert cancel_order_mock.call_count == 1
     assert trade.amount == filled_amount
 
@@ -766,8 +769,8 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
         }
     )
     # check that the trade is called, which is done by ensuring exchange.cancel_order is called
-    msg = rpc._rpc_forcesell('2')
-    assert msg == {'result': 'Created sell order for trade 2.'}
+    msg = rpc._rpc_forcesell('4')
+    assert msg == {'result': 'Created sell order for trade 4.'}
     assert cancel_order_mock.call_count == 2
     assert trade.amount == amount
 
diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py
--- 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),
+        _is_dry_limit_order_filled=MagicMock(return_value=False),
     )
     patch_get_signal(ftbot, (True, False))
 
diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py
--- 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,
+        _is_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,
+        _is_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,
+        _is_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,
+        _is_dry_limit_order_filled=MagicMock(return_value=True),
     )
     default_conf['max_open_trades'] = 4
     freqtradebot = FreqtradeBot(default_conf)
@@ -805,9 +809,9 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None
     context.args = ["all"]
     telegram._forcesell(update=update, context=context)
 
-    # Called for each trade 4 times
-    assert msg_mock.call_count == 12
-    msg = msg_mock.call_args_list[2][0][0]
+    # Called for each trade 2 times
+    assert msg_mock.call_count == 8
+    msg = msg_mock.call_args_list[1][0][0]
     assert {
         'type': RPCMessageType.SELL,
         'trade_id': 1,
diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -305,6 +305,7 @@ def test_total_open_trades_stakes(mocker, default_conf, ticker, fee) -> None:
         'freqtrade.exchange.Exchange',
         fetch_ticker=ticker,
         get_fee=fee,
+        _is_dry_limit_order_filled=MagicMock(return_value=False),
     )
     freqtrade = FreqtradeBot(default_conf)
     patch_get_signal(freqtrade)
@@ -334,6 +335,7 @@ def test_create_trade(default_conf, ticker, limit_buy_order, fee, mocker) -> Non
         'freqtrade.exchange.Exchange',
         fetch_ticker=ticker,
         get_fee=fee,
+        _is_dry_limit_order_filled=MagicMock(return_value=False),
     )
 
     # Save state of current whitelist
@@ -2533,6 +2535,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N
         'freqtrade.exchange.Exchange',
         fetch_ticker=ticker,
         get_fee=fee,
+        _is_dry_limit_order_filled=MagicMock(return_value=False),
     )
     patch_whitelist(mocker, default_conf)
     freqtrade = FreqtradeBot(default_conf)
@@ -2596,6 +2599,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker)
         'freqtrade.exchange.Exchange',
         fetch_ticker=ticker,
         get_fee=fee,
+        _is_dry_limit_order_filled=MagicMock(return_value=False),
     )
     patch_whitelist(mocker, default_conf)
     freqtrade = FreqtradeBot(default_conf)
@@ -2648,6 +2652,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe
         'freqtrade.exchange.Exchange',
         fetch_ticker=ticker,
         get_fee=fee,
+        _is_dry_limit_order_filled=MagicMock(return_value=False),
     )
     patch_whitelist(mocker, default_conf)
     freqtrade = FreqtradeBot(default_conf)
@@ -2750,6 +2755,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,
+        _is_dry_limit_order_filled=MagicMock(side_effect=[True, False]),
     )
 
     freqtrade = FreqtradeBot(default_conf)
@@ -2792,6 +2798,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,
+        _is_dry_limit_order_filled=MagicMock(side_effect=[False, True]),
     )
 
     stoploss = MagicMock(return_value={
@@ -2860,6 +2867,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee,
         'freqtrade.exchange.Exchange',
         fetch_ticker=ticker,
         get_fee=fee,
+        _is_dry_limit_order_filled=MagicMock(return_value=False),
     )
     patch_whitelist(mocker, default_conf)
     freqtrade = FreqtradeBot(default_conf)
@@ -3465,6 +3473,7 @@ def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, limit_b
         }),
         buy=MagicMock(return_value=limit_buy_order_open),
         get_fee=fee,
+        _is_dry_limit_order_filled=MagicMock(return_value=False),
     )
     default_conf['ask_strategy'] = {
         'ignore_roi_if_buy_signal': False