diff --git a/freqtrade/main.py b/freqtrade/main.py index 3b365f549..b9150f1dc 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -180,21 +180,66 @@ def handle_timedout_limit_sell(trade: Trade, order: Dict) -> bool: Sell timeout - cancel order and update trade :return: True if order was fully cancelled """ - if order['remaining'] == order['amount']: - # if trade is not partially completed, just cancel the trade - exchange.cancel_order(trade.open_order_id, trade.pair) - trade.close_rate = None - trade.close_profit = None - trade.close_date = None - trade.is_open = True - trade.open_order_id = None - rpc.send_msg('*Timeout:* Unfilled sell order for {} cancelled'.format( - trade.pair.replace('_', '/'))) - logger.info('Sell order timeout for %s.', trade) - return True + logger.info('Sell order timeout for %s.', trade) - # TODO: figure out how to handle partially complete sell orders - return False + # Partial filled sell order timed out + if order['remaining'] < order['amount']: + + (min_qty, max_qty, step_qty) = exchange.get_trade_qty(trade.pair) + + # Create new trade for partial filled amount and close that new trade + new_trade_amount = order['amount'] - order['remaining'] + + if min_qty: + # Remaining amount must be exchange minimum order quantity to be able to sell it + if new_trade_amount < min_qty: + logger.info('Wont cancel partial filled sell order that timed out for {}:'.format( + trade) + + 'remaining amount {} too low for new order '.format(new_trade_amount) + + '(minimum order quantity: {})'.format(new_trade_amount, min_qty)) + return False + + exchange.cancel_order(trade.open_order_id, trade.pair) + + new_trade_stake_amount = new_trade_amount * trade.open_rate + + # but give it half fee: because we share buy order with current trade + # this trade only costs sell fee + new_trade = Trade( + pair=trade.pair, + stake_amount=new_trade_stake_amount, + amount=new_trade_amount, + fee=(trade.fee/2), + open_rate=trade.open_rate, + open_date=trade.open_date, + exchange=trade.exchange, + open_order_id=None + ) + new_trade.close(order['rate']) + + # Update stake and amount leftover of current trade to still be handled + trade.amount = order['remaining'] + trade.stake_amount = trade.amount * trade.open_rate + trade.open_order_id = None + + rpc.send_msg('*Timeout:* Partially filled sell order for {} cancelled: '.format( + trade.pair.replace('_', '/')) + + '{} amount remains'.format(trade.amount)) + + return False + + # Order is not partially filled: full amount remains + # Just remove the order and the trade remains to be handled + exchange.cancel_order(trade.open_order_id, trade.pair) + trade.close_rate = None + trade.close_profit = None + trade.close_date = None + trade.is_open = True + trade.open_order_id = None + rpc.send_msg('*Timeout:* Unfilled sell order for {} cancelled'.format( + trade.pair.replace('_', '/'))) + + return True def check_handle_timedout(timeoutvalue: int) -> None: diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 79903d66f..1b8bab2bd 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -133,7 +133,7 @@ class Trade(_DECL_BASE): self.is_open = False self.open_order_id = None logger.info( - 'Marking %s as closed as the trade is fulfilled and found no open orders for it.', + 'Marking %s as closed since found no open orders for it.', self ) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 2b1d14268..d1117e608 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -184,6 +184,19 @@ def limit_buy_order_old_partial(): } +@pytest.fixture +def limit_sell_order_old_partial(): + return { + 'id': 'mocked_limit_sell_old_partial', + 'type': 'LIMIT_SELL', + 'pair': 'BTC_ETH', + 'opened': str(arrow.utcnow().shift(minutes=-601).datetime), + 'rate': 0.00001099, + 'amount': 90.99181073, + 'remaining': 67.99181073, + } + + @pytest.fixture def limit_sell_order(): return { diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index 1adfa8418..134f912e7 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -535,14 +535,50 @@ def test_handle_timedout_limit_sell(mocker): 'amount': 1} assert main.handle_timedout_limit_sell(trade, order) assert cancel_order.call_count == 1 - order['amount'] = 2 - assert not main.handle_timedout_limit_sell(trade, order) - # Assert cancel_order was not called (callcount remains unchanged) - assert cancel_order.call_count == 1 -def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old_partial, - mocker): +def test_check_handle_timedout_partial_sell(default_conf, ticker, limit_sell_order_old_partial, + mocker): + mocker.patch.dict('freqtrade.main._CONF', default_conf) + cancel_order_mock = MagicMock() + get_trade_qty_mock = MagicMock(return_value=(None, None, None)) + mocker.patch('freqtrade.rpc.init', MagicMock()) + rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) + mocker.patch.multiple('freqtrade.main.exchange', + validate_pairs=MagicMock(), + get_ticker=ticker, + get_order=MagicMock(return_value=limit_sell_order_old_partial), + cancel_order=cancel_order_mock, + get_trade_qty=get_trade_qty_mock) + init(default_conf, create_engine('sqlite://')) + + trade_sell = Trade( + pair='BTC_ETH', + open_rate=0.00001099, + exchange='BITTREX', + open_order_id='123456789', + amount=90.99181073, + fee=0.0, + stake_amount=1, + open_date=arrow.utcnow().shift(minutes=-601).datetime, + is_open=True + ) + + Trade.session.add(trade_sell) + + # check it does cancel sell orders over the time limit + # note this is for a partially-complete sell order + check_handle_timedout(600) + assert cancel_order_mock.call_count == 1 + assert rpc_mock.call_count == 1 + trades = Trade.query.filter(Trade.open_order_id.is_(trade_sell.open_order_id)).all() + assert len(trades) == 1 + assert trades[0].amount == 67.99181073 + assert trades[0].stake_amount == trade_sell.open_rate * trades[0].amount + + +def test_check_handle_timedout_partial_buy(default_conf, ticker, limit_buy_order_old_partial, + mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) cancel_order_mock = MagicMock() mocker.patch('freqtrade.rpc.init', MagicMock())