Repository: freqtrade/freqtrade

Issue:
Wrong stake amount after partial sell if canceled on exchange while only partly-filled
## Describe your environment

  * Operating system: Debian "buster" 11.5
  * Python Version: 3.9.2
  * CCXT version: 1.93.66 (may not be correct!  I updated freqtrade before filing this issue)
  * Freqtrade Version: `develop` branch, git commit 93cbfc5f294afc4e89e73d0161d024bf07ad6cf3 (this bit IS correct)
  
## Describe the problem:

After a series of actions that seemed normal to me at first, I ended up with a trade that freqtrade could not sell, because it thought it was supposed to sell a larger stake than it actually had.  In the name of privacy, the name and quantities of the coin in question have been obfuscated, this being a public venue and all.  That said, the stake amounts involved were well in excess of the exchange's minimum limits, so that isn't the cause.

To start off, the strategy bought 200 COIN through its normal process, i.e. just going about its usual business of buying and selling.  It held onto this trade for about a day before I stuck my nose in. :stuck_out_tongue:  

I decided I needed some money for something else, so I did a partial sell of 50 COIN.  The partial sell filled normally, and generated "exiting" and "exited" notifications in Telegram, as usual.  This partial sell left 150 COIN, exactly as expected.

Immediately after, I realized I needed a bit more money for that other thing, so I did another partial sell of 50 COIN.  The bot generated an "exiting" notification for this action, as well.

That order was taking forever to fill, but it did get far enough along to meet my immediate need, so I canceled the order via the exchange with 10 COIN left to go, leaving the trade with a final stake of 110 COIN.

Canceling the order there caused freqtrade to issue a "canceled" notification, just expected, but the partial sell action itself \*did not\* generate the "exited" notification like with the first partial sell.  I didn't initially notice this, though (not until later, when I skimmed through Telegram in pursuit of this issue).

Later, when I saw that COIN was just coming to the end of a nice pump, I knew the strategy would probably miss the optimal exit, so I took profit and tried to close-out that trade entirely.  That was with a routine force exit, not a partial sell, same as any other day.

That should have issued a sell order for 110 COIN, but the bot did not generate an "exiting" notification, which prompted me to check the log.  Therein, I saw that the bot was trying to sell *150* COIN, i.e. the amount that remained after the first partial sell filled, thus it was unable to close the trade:

```
2022-09-19 00:24:09,298 - freqtrade.freqtradebot - INFO - Exit for COIN/XYZ detected. Reason: custom_exit
2022-09-19 00:24:09,519 - freqtrade.wallets - INFO - Wallets synced.
2022-09-19 00:24:09,519 - freqtrade.freqtradebot - WARNING - Unable to exit trade COIN/XYZ: Not enough amount to exit trade. Trade-amount: 150, Wallet: 110
2022-09-19 00:24:18,822 - freqtrade.worker - INFO - Bot heartbeat. PID=305215, version='2022.9.dev-93cbfc5f2, strategy_version: v11.2.140', state='RUNNING'
```

I did a `reload_config` to try to prod the bot to correct the stake amount and close the trade, but that didn't help.  I had to delete the trade entirely and sell it via the exchange.

There were no rebuys on this particular trade.  Just the initial buy and the above-described sells.

### Steps to reproduce:

I am not sure if this is repeatable....

1. Buy something, preferably some low-volume coin that takes a long time to fill.
2. Hold it for a while, maybe even just 5 minutes.
3. Do a partial sell via frequi.
4. Do another partial sell via frequi.
5. Go on the exchange and cancel that order *before* it can completely fill.
6. Hold the remainder for a while, say another 5 minutes.
7. Do a final force-exit via frequi.

### Observed Results:

  * What happened?
  * What did you expect to happen?

The bot lost track of how much it could actually sell, and thus could not close-out the trade.  Obviously, it should have tracked the amount properly at all times :smiley:, but perhaps as a backup, it should automatically use the amount the exchange wallet actually has in it, if that's less than the assumed stake amount.

### Relevant code exceptions or logs

Just before the bot went down for the aforementioned restart, it threw this python error:

<details><summary><b>First python error...</b></summary>

```
2022-09-18 23:29:05,628 - uvicorn.error - ERROR - Exception in ASGI application
Traceback (most recent call last):
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/uvicorn/protocols/http/h11_impl.py", line 404, in run_asgi
    result = await app(  # type: ignore[func-returns-value]
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/uvicorn/middleware/proxy_headers.py", line 78, in __call__
    return await self.app(scope, receive, send)
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/uvicorn/middleware/message_logger.py", line 86, in __call__
    raise exc from None
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/uvicorn/middleware/message_logger.py", line 82, in __call__
    await self.app(scope, inner_receive, inner_send)
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/fastapi/applications.py", line 269, in __call__
    await super().__call__(scope, receive, send)
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/starlette/applications.py", line 124, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/starlette/middleware/errors.py", line 184, in __call__
    raise exc
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/starlette/middleware/errors.py", line 162, in __call__
    await self.app(scope, receive, _send)
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/starlette/middleware/cors.py", line 92, in __call__
    await self.simple_response(scope, receive, send, request_headers=headers)
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/starlette/middleware/cors.py", line 147, in simple_response
    await self.app(scope, receive, send)
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/starlette/exceptions.py", line 93, in __call__
    raise exc
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/starlette/exceptions.py", line 82, in __call__
    await self.app(scope, receive, sender)
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/fastapi/middleware/asyncexitstack.py", line 21, in __call__
    raise e
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/fastapi/middleware/asyncexitstack.py", line 18, in __call__
    await self.app(scope, receive, send)
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/starlette/routing.py", line 670, in __call__
    await route.handle(scope, receive, send)
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/starlette/routing.py", line 266, in handle
    await self.app(scope, receive, send)
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/starlette/routing.py", line 65, in app
    response = await func(request)
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/fastapi/routing.py", line 231, in app
    raw_response = await run_endpoint_function(
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/fastapi/routing.py", line 162, in run_endpoint_function
    return await run_in_threadpool(dependant.call, **values)
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/starlette/concurrency.py", line 41, in run_in_threadpool
    return await anyio.to_thread.run_sync(func, *args)
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/anyio/to_thread.py", line 31, in run_sync
    return await get_asynclib().run_sync_in_worker_thread(
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/anyio/_backends/_asyncio.py", line 937, in run_sync_in_worker_thread
    return await future
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/anyio/_backends/_asyncio.py", line 867, in run
    result = context.run(func, *args)
  File "/home/vanessa/freqtrade-related/freqtrade-spot/freqtrade/rpc/api_server/api_v1.py", line 164, in forceexit
    return rpc._rpc_force_exit(payload.tradeid, ordertype, amount=payload.amount)
  File "/home/vanessa/freqtrade-related/freqtrade-spot/freqtrade/rpc/rpc.py", line 733, in _rpc_force_exit
    self.__exec_force_exit(trade, ordertype, amount)
  File "/home/vanessa/freqtrade-related/freqtrade-spot/freqtrade/rpc/rpc.py", line 702, in __exec_force_exit
    self._freqtrade.execute_trade_exit(
  File "/home/vanessa/freqtrade-related/freqtrade-spot/freqtrade/freqtradebot.py", line 1560, in execute_trade_exit
    self._notify_exit(trade, order_type, sub_trade=bool(sub_trade_amt), order=order_obj)
  File "/home/vanessa/freqtrade-related/freqtrade-spot/freqtrade/freqtradebot.py", line 1622, in _notify_exit
    self.rpc.send_msg(msg)
  File "/home/vanessa/freqtrade-related/freqtrade-spot/freqtrade/rpc/rpc_manager.py", line 77, in send_msg
    mod.send_msg(msg)
  File "/home/vanessa/freqtrade-related/freqtrade-spot/freqtrade/rpc/telegram.py", line 441, in send_msg
    message = self.compose_message(msg, msg_type)
  File "/home/vanessa/freqtrade-related/freqtrade-spot/freqtrade/rpc/telegram.py", line 382, in compose_message
    message = self._format_exit_msg(msg)
  File "/home/vanessa/freqtrade-related/freqtrade-spot/freqtrade/rpc/telegram.py", line 355, in _format_exit_msg
    message += (f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
TypeError: unsupported format string passed to NoneType.__format__
```
</details>

Digging through the logs as requested, I found nothing that suggested a fault with opening either of the sell orders, but the bot did keep repeating the aforementioned stake amount vs. wallet warning every so often.

In the midst of those repeated warnings, I found this python error:

<details><summary><b>Second python error...</b></summary>

```
2022-09-19 14:01:51,823 - freqtrade.freqtradebot - WARNING - Unable to exit trade COIN/XYZ: Not enough amount to exit trade. Trade-amount: 150, Wallet: 110
2022-09-19 14:01:53,524 - freqtrade.wallets - INFO - Wallets synced.
2022-09-19 14:01:53,525 - uvicorn.error - ERROR - Exception in ASGI application
Traceback (most recent call last):
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/uvicorn/protocols/http/h11_impl.py", line 404, in run_asgi
    result = await app(  # type: ignore[func-returns-value]
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/uvicorn/middleware/proxy_headers.py", line 78, in __call__
    return await self.app(scope, receive, send)
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/uvicorn/middleware/message_logger.py", line 86, in __call__
    raise exc from None
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/uvicorn/middleware/message_logger.py", line 82, in __call__
    await self.app(scope, inner_receive, inner_send)
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/fastapi/applications.py", line 269, in __call__
    await super().__call__(scope, receive, send)
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/starlette/applications.py", line 124, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/starlette/middleware/errors.py", line 184, in __call__
    raise exc
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/starlette/middleware/errors.py", line 162, in __call__
    await self.app(scope, receive, _send)
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/starlette/middleware/cors.py", line 92, in __call__
    await self.simple_response(scope, receive, send, request_headers=headers)
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/starlette/middleware/cors.py", line 147, in simple_response
    await self.app(scope, receive, send)
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/starlette/exceptions.py", line 93, in __call__
    raise exc
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/starlette/exceptions.py", line 82, in __call__
    await self.app(scope, receive, sender)
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/fastapi/middleware/asyncexitstack.py", line 21, in __call__
    raise e
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/fastapi/middleware/asyncexitstack.py", line 18, in __call__
    await self.app(scope, receive, send)
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/starlette/routing.py", line 670, in __call__
    await route.handle(scope, receive, send)
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/starlette/routing.py", line 266, in handle
    await self.app(scope, receive, send)
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/starlette/routing.py", line 65, in app
    response = await func(request)
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/fastapi/routing.py", line 231, in app
    raw_response = await run_endpoint_function(
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/fastapi/routing.py", line 162, in run_endpoint_function
    return await run_in_threadpool(dependant.call, **values)
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/starlette/concurrency.py", line 41, in run_in_threadpool
    return await anyio.to_thread.run_sync(func, *args)
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/anyio/to_thread.py", line 31, in run_sync
    return await get_asynclib().run_sync_in_worker_thread(
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/anyio/_backends/_asyncio.py", line 937, in run_sync_in_worker_thread
    return await future
  File "/home/vanessa/freqtrade-related/freqtrade-spot/.env/lib/python3.9/site-packages/anyio/_backends/_asyncio.py", line 867, in run
    result = context.run(func, *args)
  File "/home/vanessa/freqtrade-related/freqtrade-spot/freqtrade/rpc/api_server/api_v1.py", line 164, in forceexit
    return rpc._rpc_force_exit(payload.tradeid, ordertype, amount=payload.amount)
  File "/home/vanessa/freqtrade-related/freqtrade-spot/freqtrade/rpc/rpc.py", line 733, in _rpc_force_exit
    self.__exec_force_exit(trade, ordertype, amount)
  File "/home/vanessa/freqtrade-related/freqtrade-spot/freqtrade/rpc/rpc.py", line 702, in __exec_force_exit
    self._freqtrade.execute_trade_exit(
  File "/home/vanessa/freqtrade-related/freqtrade-spot/freqtrade/freqtradebot.py", line 1515, in execute_trade_exit
    amount = self._safe_exit_amount(trade.pair, sub_trade_amt or trade.amount)
  File "/home/vanessa/freqtrade-related/freqtrade-spot/freqtrade/freqtradebot.py", line 1463, in _safe_exit_amount
    raise DependencyException(
freqtrade.exceptions.DependencyException: Not enough amount to exit trade. Trade-amount: 150, Wallet: 110

```
</details>

I found nothing else in the log that suggested there was a problem.


Discussion:
Just close this trade manually by accessing the database and type:

```
update trades set is_open=0 where id={get trade id from telegram}
```
You may also want to remove from orders:
```
delete from orders where order_id={get order_id from logs}
```
or they can just delete the trade from telegram easily, but that's not the point here
partially filled exit orders are currently not supported.

Freqtrade would never cancel this (there's currently no logic to handle partially filled exits).
Now obviously we can't prevent users from canceling such an order on exchange - but that still doesn't change the fact that there's no logic to handle this case.

To some degree, this makes this a duplicate of #3009. 
While that issue is around "freqtrade won't cancel my exit" - the point is really the same - we currently don't handle partially filled exits.

The "real" interesting thing would be the log messages from around the time the partial (and partially filled) sell order was canceled.

Feel free to either share here or via DM.
(detailed logs relayed via DM)
It seems that any partial sell order has the potential to "succeed silently"...

I had it happen with another trade after filing this issue, and THAT one was completely routine -- no cancellations, no slow fills, no funny business.  Just the usual entry, a few "rebuys", and a couple of partial sell attempts.

No database entries were created for the sell orders, and no Telegram notifications ware generated, yet the orders fully filled on the exchange, got recorded into the ledger there, and the open trade was left showing a larger stake amount than it should have, exactly like my initial issue.


> No database entries were created for the sell orders, and no Telegram notifications ware generated, yet the orders fully filled on the exchange, got recorded into the ledger there, and the open trade was left showing a larger stake amount than it should have, exactly like my initial issue.

without logs - this will be a tricky one to even attempt to debug.

**edit** i now understand that you think it's the same issue ...

Resolving patch:
diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py
--- a/freqtrade/exchange/exchange.py
+++ b/freqtrade/exchange/exchange.py
@@ -1292,7 +1292,14 @@ def cancel_order_with_result(self, order_id: str, pair: str, amount: float) -> D
             order = self.fetch_order(order_id, pair)
         except InvalidOrderException:
             logger.warning(f"Could not fetch cancelled order {order_id}.")
-            order = {'id': order_id, 'fee': {}, 'status': 'canceled', 'amount': amount, 'info': {}}
+            order = {
+                'id': order_id,
+                'status': 'canceled',
+                'amount': amount,
+                'filled': 0.0,
+                'fee': {},
+                'info': {}
+            }
 
         return order
 
diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -1409,47 +1409,63 @@ def handle_cancel_exit(self, trade: Trade, order: Dict, reason: str) -> bool:
         :return: True if exit order was cancelled, false otherwise
         """
         cancelled = False
-        # if trade is not partially completed, just cancel the order
-        if order['remaining'] == order['amount'] or order.get('filled') == 0.0:
-            if not self.exchange.check_order_canceled_empty(order):
-                try:
-                    # if trade is not partially completed, just delete the order
-                    co = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair,
-                                                                trade.amount)
-                    trade.update_order(co)
-                except InvalidOrderException:
-                    logger.exception(
-                        f"Could not cancel {trade.exit_side} order {trade.open_order_id}")
+        # Cancelled orders may have the status of 'canceled' or 'closed'
+        if order['status'] not in constants.NON_OPEN_EXCHANGE_STATES:
+            filled_val: float = order.get('filled', 0.0) or 0.0
+            filled_rem_stake = trade.stake_amount - filled_val * trade.open_rate
+            minstake = self.exchange.get_min_pair_stake_amount(
+                trade.pair, trade.open_rate, self.strategy.stoploss)
+            # Double-check remaining amount
+            if filled_val > 0:
+                reason = constants.CANCEL_REASON['PARTIALLY_FILLED']
+                if minstake and filled_rem_stake < minstake:
+                    logger.warning(
+                        f"Order {trade.open_order_id} for {trade.pair} not cancelled, as "
+                        f"the filled amount of {filled_val} would result in an unexitable trade.")
+                    reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
+
+                    self._notify_exit_cancel(
+                        trade,
+                        order_type=self.strategy.order_types['exit'],
+                        reason=reason, order_id=order['id'],
+                        sub_trade=trade.amount != order['amount']
+                    )
                     return False
-                logger.info('%s order %s for %s.', trade.exit_side.capitalize(), reason, trade)
-            else:
-                reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
-                logger.info('%s order %s for %s.', trade.exit_side.capitalize(), reason, trade)
-                trade.update_order(order)
 
+            try:
+                co = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair,
+                                                            trade.amount)
+            except InvalidOrderException:
+                logger.exception(
+                    f"Could not cancel {trade.exit_side} order {trade.open_order_id}")
+                return False
             trade.close_rate = None
             trade.close_rate_requested = None
             trade.close_profit = None
             trade.close_profit_abs = None
-            trade.open_order_id = None
-            trade.exit_reason = None
+            # Set exit_reason for fill message
+            exit_reason_prev = trade.exit_reason
+            trade.exit_reason = trade.exit_reason + f", {reason}" if trade.exit_reason else reason
+            self.update_trade_state(trade, trade.open_order_id, co)
+            # Order might be filled above in odd timing issues.
+            if co.get('status') in ('canceled', 'cancelled'):
+                trade.exit_reason = None
+            else:
+                trade.exit_reason = exit_reason_prev
+
+            logger.info(f'{trade.exit_side.capitalize()} order {reason} for {trade}.')
             cancelled = True
-            self.wallets.update()
         else:
-            # TODO: figure out how to handle partially complete sell orders
-            reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
-            cancelled = False
+            reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
+            logger.info(f'{trade.exit_side.capitalize()} order {reason} for {trade}.')
+            self.update_trade_state(trade, trade.open_order_id, order)
 
-        order_obj = trade.select_order_by_order_id(order['id'])
-        if not order_obj:
-            raise DependencyException(
-                f"Order_obj not found for {order['id']}. This should not have happened.")
+        self.wallets.update()
 
-        sub_trade = order_obj.amount != trade.amount
         self._notify_exit_cancel(
             trade,
             order_type=self.strategy.order_types['exit'],
-            reason=reason, order=order_obj, sub_trade=sub_trade
+            reason=reason, order_id=order['id'], sub_trade=trade.amount != order['amount']
         )
         return cancelled
 
@@ -1646,7 +1662,7 @@ def _notify_exit(self, trade: Trade, order_type: str, fill: bool = False,
         self.rpc.send_msg(msg)
 
     def _notify_exit_cancel(self, trade: Trade, order_type: str, reason: str,
-                            order: Order, sub_trade: bool = False) -> None:
+                            order_id: str, sub_trade: bool = False) -> None:
         """
         Sends rpc notification when a sell cancel occurred.
         """
@@ -1655,6 +1671,11 @@ def _notify_exit_cancel(self, trade: Trade, order_type: str, reason: str,
         else:
             trade.exit_order_status = reason
 
+        order = trade.select_order_by_order_id(order_id)
+        if not order:
+            raise DependencyException(
+                f"Order_obj not found for {order_id}. This should not have happened.")
+
         profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
         profit_trade = trade.calc_profit(rate=profit_rate)
         current_rate = self.exchange.get_rate(


Test patch:
diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -3095,6 +3095,9 @@ def test_handle_cancel_enter_corder_empty(mocker, default_conf_usdt, limit_order
 
     cancel_order_mock.reset_mock()
     l_order['filled'] = 1.0
+    order = deepcopy(l_order)
+    order['status'] = 'canceled'
+    mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order)
     assert not freqtrade.handle_cancel_enter(trade, l_order, reason)
     assert cancel_order_mock.call_count == 1
 
@@ -3108,6 +3111,9 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None:
         cancel_order=cancel_order_mock,
     )
     mocker.patch('freqtrade.exchange.Exchange.get_rate', return_value=0.245441)
+    mocker.patch('freqtrade.exchange.Exchange.get_min_pair_stake_amount', return_value=0.2)
+
+    mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_order_fee')
 
     freqtrade = FreqtradeBot(default_conf_usdt)
 
@@ -3175,7 +3181,9 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None:
 
     send_msg_mock.reset_mock()
 
+    # Partial exit - below exit threshold
     order['amount'] = 2
+    order['filled'] = 1.9
     assert not freqtrade.handle_cancel_exit(trade, order, reason)
     # Assert cancel_order was not called (callcount remains unchanged)
     assert cancel_order_mock.call_count == 1
@@ -3185,12 +3193,21 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None:
 
     assert not freqtrade.handle_cancel_exit(trade, order, reason)
 
-    send_msg_mock.call_args_list[0][0][0]['reason'] = CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
+    assert (send_msg_mock.call_args_list[0][0][0]['reason']
+            == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'])
 
     # Message should not be iterated again
     assert trade.exit_order_status == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
     assert send_msg_mock.call_count == 1
 
+    send_msg_mock.reset_mock()
+
+    order['filled'] = 1
+    assert freqtrade.handle_cancel_exit(trade, order, reason)
+    assert send_msg_mock.call_count == 1
+    assert (send_msg_mock.call_args_list[0][0][0]['reason']
+            == CANCEL_REASON['PARTIALLY_FILLED'])
+
 
 def test_handle_cancel_exit_cancel_exception(mocker, default_conf_usdt) -> None:
     patch_RPCManager(mocker)