Repository: freqtrade/freqtrade

Issue:
Don't cancel partial filled buy order if minimum order amount hasn't been reached.
# Describe the enhancement

Don't cancel partial filled buy orders if the minimum exchange order amount hasn't been reached.
I use a stake amount of 15 USDT, which is close to the minimum order amount of 10 USDT on Binance. Whenever a buy order is partially fullfilled with less than < 10 USDT, the bot can not only sell this pair anymore, but the user will have to manually take care of it, sell it on the exchange itself and delete it from the database.

Of course the discussion point would be then: there would be cases where it doesn't get fulfilled, because the price has gone too far up/or down, do you just leave it open for days? I don't think that's the best solution, but maybe for a couple of hours, before cancelling it? 




Discussion:
This is a duplicate of #2877 (and to some extend also of #3009 - which is for sell-side - but the problem is really similar on both sides). 
Please use the search function to find related / similar issues before opening new ones.

the problem with "ever" cancelling these is that they leave you with binance dust - unsellable assets you can only create to BNB.

Now there are options to convert to BNB - but that's binance-specific - however the problem exists on other exchanges (kraken, Ftx) just in the same way - which don't have a way to handle these assets other than "keeping" them forever.

Details for this are in the following comment https://github.com/freqtrade/freqtrade/issues/2877#issuecomment-583337160
`Please use the search function to find related / similar issues before opening new ones.` Yes, but the one you linked is more of described as a bug, and I didn't really see anything there about keeping these orders open until MIN_NOTIONAL has been reached (only the last comment, but no one has ever responded to that). 

Also I tried the workaround, but that's no solution for me, it always takes around 5 seconds to 2 minutes before most of my orders gets fulfilled, I haven't had 1 FOK order that got fulfilled. 

`the problem with "ever" cancelling these is that they leave you with binance dust - unsellable assets you can only create to BNB.` I'm not sure what this has to do with my enhancement, I'm proposing the exact opposite, to NOT cancel them. I'm also not saying to convert it to BNB. Because this is the current behaviour of the bot, it cancels them, which leaves you with dust, with this enhancement it will try to prevent this.

How this enhancement could work is as following:
* Buy is partial fulfilled
* Bot is about to cancel the order (due to timeout)
* Check if MIN_NOTIONAL has been reached (for either the asset or the stake amount, if API returns this to begin with), if not, keep order open until it has.
* Order can't stay open forever, if the order doesn't fulfill within.. let's say 1 to 2 hours maybe? then cancel the partial filled order after all and notify the user that the bot has an unsellable asset. 

And the reason for this is because, if MIN_NOTIONAL has not been reached, the pair is **completely useless** for the bot, it can't do anything with it. And next to that, it will also hold an open trade forever, until you manually delete it from the database.


Resolving patch:
diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -1045,6 +1045,16 @@ def handle_cancel_buy(self, trade: Trade, order: Dict, reason: str) -> bool:
 
         # Cancelled orders may have the status of 'canceled' or 'closed'
         if order['status'] not in ('cancelled', 'canceled', 'closed'):
+            filled_val = order.get('filled', 0.0) or 0.0
+            filled_stake = filled_val * trade.open_rate
+            minstake = self.exchange.get_min_pair_stake_amount(
+                trade.pair, trade.open_rate, self.strategy.stoploss)
+
+            if filled_val > 0 and filled_stake < minstake:
+                logger.warning(
+                    f"Order {trade.open_order_id} for {trade.pair} not cancelled, "
+                    f"as the filled amount of {filled_val} would result in an unsellable trade.")
+                return False
             corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair,
                                                             trade.amount)
             # Avoid race condition where the order could not be cancelled coz its already filled.


Test patch:
diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -2431,13 +2431,22 @@ def test_handle_cancel_buy(mocker, caplog, default_conf, limit_buy_order) -> Non
     freqtrade._notify_buy_cancel = MagicMock()
 
     trade = MagicMock()
-    trade.pair = 'LTC/ETH'
+    trade.pair = 'LTC/USDT'
+    trade.open_rate = 200
     limit_buy_order['filled'] = 0.0
     limit_buy_order['status'] = 'open'
     reason = CANCEL_REASON['TIMEOUT']
     assert freqtrade.handle_cancel_buy(trade, limit_buy_order, reason)
     assert cancel_order_mock.call_count == 1
 
+    cancel_order_mock.reset_mock()
+    caplog.clear()
+    limit_buy_order['filled'] = 0.01
+    assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason)
+    assert cancel_order_mock.call_count == 0
+    assert log_has_re("Order .* for .* not cancelled, as the filled amount.* unsellable.*", caplog)
+
+    caplog.clear()
     cancel_order_mock.reset_mock()
     limit_buy_order['filled'] = 2
     assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason)
@@ -2492,7 +2501,8 @@ def test_handle_cancel_buy_corder_empty(mocker, default_conf, limit_buy_order,
     freqtrade._notify_buy_cancel = MagicMock()
 
     trade = MagicMock()
-    trade.pair = 'LTC/ETH'
+    trade.pair = 'LTC/USDT'
+    trade.open_rate = 200
     limit_buy_order['filled'] = 0.0
     limit_buy_order['status'] = 'open'
     reason = CANCEL_REASON['TIMEOUT']