Implement partial sell order timeout handling
This commit is contained in:
parent
67b4af5ec4
commit
691b4fae6f
@ -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:
|
||||
|
@ -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
|
||||
)
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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())
|
||||
|
Loading…
Reference in New Issue
Block a user