Repository: freqtrade/freqtrade

Issue:
`sell_fill` Telegram message gets send twice
## Describe your environment

  * Operating system: Debian x11
  * Python Version: 3.9.2 
  * CCXT version: 1.61.92
  * Freqtrade Version: 2021.11
  

## Describe the problem:

The sell fill message gets send twice, see screenshot. I only expect it to be send one time, once the order gets fulfilled.
![image](https://user-images.githubusercontent.com/24569139/143784929-10d1d1b2-64ca-44a7-b8d8-0ed1f55cda69.png)

### Steps to reproduce:

  1. Set `sell_fill` to `on` in the Telegram config part
  2. Wait for your bot to make a trade and fill it, I am running live (maybe that matters)
  
### Observed Results:

  * What happened?
The fill sell message got sent twice.

  * What did you expect to happen?
I expect the fill sell message to only send once.

### Relevant code exceptions or logs

As you can see in the logs, it also says here that it sends the sell_fill message twice.

  ```
2021-11-28 17:52:26,512 - freqtrade.wallets - INFO - Wallets synced.
2021-11-28 17:52:26,550 - freqtrade.freqtradebot - INFO - Found open order for Trade(id=1, pair=REQ/USDT, amount=35.88300000, open_rate=0.54161000, open_since=2021-11-28 16:26:47)
2021-11-28 17:52:26,580 - freqtrade.freqtradebot - INFO - Applying fee on amount for Trade(id=1, pair=REQ/USDT, amount=35.88300000, open_rate=0.54161000, open_since=2021-11-28 16:26:47) failed: myTrade-Dict empty found
2021-11-28 17:52:26,580 - freqtrade.persistence.models - INFO - Updating trade (id=1) ...
2021-11-28 17:52:26,580 - freqtrade.persistence.models - INFO - LIMIT_SELL has been fulfilled for Trade(id=1, pair=REQ/USDT, amount=35.88300000, open_rate=0.54161000, open_since=2021-11-28 16:26:47).
2021-11-28 17:52:26,581 - freqtrade.persistence.models - INFO - Marking Trade(id=1, pair=REQ/USDT, amount=35.88300000, open_rate=0.54161000, open_since=closed) as closed as the trade is fulfilled and found no open orders for it.
2021-11-28 17:52:26,585 - freqtrade.rpc.rpc_manager - INFO - Sending rpc message: {'type': sell_fill, 'trade_id': 1, 'exchange': 'Gateio', 'pair': 'REQ/USDT', 'gain': 'profit', 'limit': 0.56028, 'order_type': '', 'amount': 35.883, 'open_rate': 0.54161, 'close_rate': 0.56028, 'current_rate': None, 'profit_amount': 0.5987652, 'profit_ratio': 0.03075389, 'buy_tag': '3 ', 'sell_reason': 'sell_profit_w_1_51 ( 3 )', 'open_date': datetime.datetime(2021, 11, 28, 16, 26, 47, 560269), 'close_date': datetime.datetime(2021, 11, 28, 17, 52, 26, 581087), 'stake_currency': 'USDT', 'fiat_currency': 'EUR'}
2021-11-28 17:52:26,865 - freqtrade.wallets - INFO - Wallets synced.
2021-11-28 17:52:26,872 - freqtrade.rpc.rpc_manager - INFO - Sending rpc message: {'type': sell, 'trade_id': 1, 'exchange': 'Gateio', 'pair': 'REQ/USDT', 'gain': 'profit', 'limit': 0.56028, 'order_type': 'limit', 'amount': 35.883, 'open_rate': 0.54161, 'close_rate': 0.56028, 'current_rate': 0.54923, 'profit_amount': 0.5987652, 'profit_ratio': 0.03075389, 'buy_tag': '3 ', 'sell_reason': 'sell_profit_w_1_51 ( 3 )', 'open_date': datetime.datetime(2021, 11, 28, 16, 26, 47, 560269), 'close_date': datetime.datetime(2021, 11, 28, 17, 52, 26, 581087), 'stake_currency': 'USDT', 'fiat_currency': 'EUR'}
2021-11-28 17:52:27,132 - freqtrade.wallets - INFO - Wallets synced.
2021-11-28 17:52:27,144 - freqtrade.freqtradebot - INFO - Pair REQ/USDT is still locked until 2021-11-28 17:55:00 due to Auto lock.
2021-11-28 17:52:33,318 - freqtrade.freqtradebot - INFO - Updating sell-fee on trade Trade(id=1, pair=REQ/USDT, amount=35.88300000, open_rate=0.54161000, open_since=closed) for order 98283895396.
2021-11-28 17:52:33,319 - freqtrade.freqtradebot - INFO - Found open order for Trade(id=1, pair=REQ/USDT, amount=35.88300000, open_rate=0.54161000, open_since=closed)
2021-11-28 17:52:33,343 - freqtrade.persistence.models - INFO - Updating trade (id=1) ...
2021-11-28 17:52:33,344 - freqtrade.persistence.models - INFO - Marking Trade(id=1, pair=REQ/USDT, amount=35.88300000, open_rate=0.54161000, open_since=closed) as closed as the trade is fulfilled and found no open orders for it.
2021-11-28 17:52:33,349 - freqtrade.rpc.rpc_manager - INFO - Sending rpc message: {'type': sell_fill, 'trade_id': 1, 'exchange': 'Gateio', 'pair': 'REQ/USDT', 'gain': 'profit', 'limit': 0.56028, 'order_type': '', 'amount': 35.883, 'open_rate': 0.54161, 'close_rate': 0.56028, 'current_rate': None, 'profit_amount': 0.5987652, 'profit_ratio': 0.03075389, 'buy_tag': '3 ', 'sell_reason': 'sell_profit_w_1_51 ( 3 )', 'open_date': datetime.datetime(2021, 11, 28, 16, 26, 47, 560269), 'close_date': datetime.datetime(2021, 11, 28, 17, 52, 26, 581087), 'stake_currency': 'USDT', 'fiat_currency': 'EUR'}
2021-11-28 17:52:33,629 - freqtrade.wallets - INFO - Wallets synced.
  ```



Discussion:

Resolving patch:
diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -278,7 +278,8 @@ def update_closed_trades_without_assigned_fees(self):
                 if order:
                     logger.info(f"Updating sell-fee on trade {trade} for order {order.order_id}.")
                     self.update_trade_state(trade, order.order_id,
-                                            stoploss_order=order.ft_order_side == 'stoploss')
+                                            stoploss_order=order.ft_order_side == 'stoploss',
+                                            send_msg=False)
 
         trades: List[Trade] = Trade.get_open_trades_without_assigned_fees()
         for trade in trades:
@@ -286,7 +287,7 @@ def update_closed_trades_without_assigned_fees(self):
                 order = trade.select_order('buy', False)
                 if order:
                     logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.")
-                    self.update_trade_state(trade, order.order_id)
+                    self.update_trade_state(trade, order.order_id, send_msg=False)
 
     def handle_insufficient_funds(self, trade: Trade):
         """
@@ -308,7 +309,7 @@ def reupdate_enter_order_fees(self, trade: Trade):
         order = trade.select_order('buy', False)
         if order:
             logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.")
-            self.update_trade_state(trade, order.order_id)
+            self.update_trade_state(trade, order.order_id, send_msg=False)
 
     def refind_lost_order(self, trade):
         """
@@ -578,10 +579,6 @@ def execute_entry(self, pair: str, stake_amount: float, price: Optional[float] =
         )
         trade.orders.append(order_obj)
 
-        # Update fees if order is closed
-        if order_status == 'closed':
-            self.update_trade_state(trade, order_id, order)
-
         Trade.query.session.add(trade)
         Trade.commit()
 
@@ -590,19 +587,25 @@ def execute_entry(self, pair: str, stake_amount: float, price: Optional[float] =
 
         self._notify_enter(trade, order_type)
 
+        # Update fees if order is closed
+        if order_status == 'closed':
+            self.update_trade_state(trade, order_id, order)
+
         return True
 
-    def _notify_enter(self, trade: Trade, order_type: str) -> None:
+    def _notify_enter(self, trade: Trade, order_type: Optional[str] = None,
+                      fill: bool = False) -> None:
         """
         Sends rpc notification when a buy occurred.
         """
         msg = {
             'trade_id': trade.id,
-            'type': RPCMessageType.BUY,
+            'type': RPCMessageType.BUY_FILL if fill else RPCMessageType.BUY,
             'buy_tag': trade.buy_tag,
             'exchange': self.exchange.name.capitalize(),
             'pair': trade.pair,
-            'limit': trade.open_rate,
+            'limit': trade.open_rate,  # Deprecated (?)
+            'open_rate': trade.open_rate,
             'order_type': order_type,
             'stake_amount': trade.stake_amount,
             'stake_currency': self.config['stake_currency'],
@@ -641,22 +644,6 @@ def _notify_enter_cancel(self, trade: Trade, order_type: str, reason: str) -> No
         # Send the message
         self.rpc.send_msg(msg)
 
-    def _notify_enter_fill(self, trade: Trade) -> None:
-        msg = {
-            'trade_id': trade.id,
-            'type': RPCMessageType.BUY_FILL,
-            'buy_tag': trade.buy_tag,
-            'exchange': self.exchange.name.capitalize(),
-            'pair': trade.pair,
-            'open_rate': trade.open_rate,
-            'stake_amount': trade.stake_amount,
-            'stake_currency': self.config['stake_currency'],
-            'fiat_currency': self.config.get('fiat_display_currency', None),
-            'amount': trade.amount,
-            'open_date': trade.open_date,
-        }
-        self.rpc.send_msg(msg)
-
 #
 # SELL / exit positions / close trades logic and methods
 #
@@ -1154,16 +1141,16 @@ def execute_trade_exit(
         trade.sell_order_status = ''
         trade.close_rate_requested = limit
         trade.sell_reason = exit_tag or sell_reason.sell_reason
-        # In case of market sell orders the order can be closed immediately
-        if order.get('status', 'unknown') in ('closed', 'expired'):
-            self.update_trade_state(trade, trade.open_order_id, order)
-        Trade.commit()
 
         # Lock pair for one candle to prevent immediate re-buys
         self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
                                 reason='Auto lock')
 
         self._notify_exit(trade, order_type)
+        # In case of market sell orders the order can be closed immediately
+        if order.get('status', 'unknown') in ('closed', 'expired'):
+            self.update_trade_state(trade, trade.open_order_id, order)
+        Trade.commit()
 
         return True
 
@@ -1260,13 +1247,14 @@ def _notify_exit_cancel(self, trade: Trade, order_type: str, reason: str) -> Non
 #
 
     def update_trade_state(self, trade: Trade, order_id: str, action_order: Dict[str, Any] = None,
-                           stoploss_order: bool = False) -> bool:
+                           stoploss_order: bool = False, send_msg: bool = True) -> bool:
         """
         Checks trades with open orders and updates the amount if necessary
         Handles closing both buy and sell orders.
         :param trade: Trade object of the trade we're analyzing
         :param order_id: Order-id of the order we're analyzing
         :param action_order: Already acquired order object
+        :param send_msg: Send notification - should always be True except in "recovery" methods
         :return: True if order has been cancelled without being filled partially, False otherwise
         """
         if not order_id:
@@ -1306,13 +1294,13 @@ def update_trade_state(self, trade: Trade, order_id: str, action_order: Dict[str
 
         # Updating wallets when order is closed
         if not trade.is_open:
-            if not stoploss_order and not trade.open_order_id:
+            if send_msg and not stoploss_order and not trade.open_order_id:
                 self._notify_exit(trade, '', True)
             self.handle_protections(trade.pair)
             self.wallets.update()
-        elif not trade.open_order_id:
+        elif send_msg and not trade.open_order_id:
             # Buy fill
-            self._notify_enter_fill(trade)
+            self._notify_enter(trade, fill=True)
 
         return False
 


Test patch:
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
@@ -937,7 +937,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee,
     telegram._forcesell(update=update, context=context)
 
     assert msg_mock.call_count == 4
-    last_msg = msg_mock.call_args_list[-1][0][0]
+    last_msg = msg_mock.call_args_list[-2][0][0]
     assert {
         'type': RPCMessageType.SELL,
         'trade_id': 1,
@@ -1001,7 +1001,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee,
 
     assert msg_mock.call_count == 4
 
-    last_msg = msg_mock.call_args_list[-1][0][0]
+    last_msg = msg_mock.call_args_list[-2][0][0]
     assert {
         'type': RPCMessageType.SELL,
         'trade_id': 1,
@@ -1055,7 +1055,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None
 
     # Called for each trade 2 times
     assert msg_mock.call_count == 8
-    msg = msg_mock.call_args_list[1][0][0]
+    msg = msg_mock.call_args_list[0][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
@@ -2979,7 +2979,7 @@ def test_execute_trade_exit_market_order(default_conf_usdt, ticker_usdt, fee,
     assert trade.close_profit == 0.09451372
 
     assert rpc_mock.call_count == 3
-    last_msg = rpc_mock.call_args_list[-1][0][0]
+    last_msg = rpc_mock.call_args_list[-2][0][0]
     assert {
         'type': RPCMessageType.SELL,
         'trade_id': 1,