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
|
Sell timeout - cancel order and update trade
|
||||||
:return: True if order was fully cancelled
|
:return: True if order was fully cancelled
|
||||||
"""
|
"""
|
||||||
if order['remaining'] == order['amount']:
|
logger.info('Sell order timeout for %s.', trade)
|
||||||
# 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
|
|
||||||
|
|
||||||
# TODO: figure out how to handle partially complete sell orders
|
# Partial filled sell order timed out
|
||||||
return False
|
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:
|
def check_handle_timedout(timeoutvalue: int) -> None:
|
||||||
|
@ -133,7 +133,7 @@ class Trade(_DECL_BASE):
|
|||||||
self.is_open = False
|
self.is_open = False
|
||||||
self.open_order_id = None
|
self.open_order_id = None
|
||||||
logger.info(
|
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
|
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
|
@pytest.fixture
|
||||||
def limit_sell_order():
|
def limit_sell_order():
|
||||||
return {
|
return {
|
||||||
|
@ -535,14 +535,50 @@ def test_handle_timedout_limit_sell(mocker):
|
|||||||
'amount': 1}
|
'amount': 1}
|
||||||
assert main.handle_timedout_limit_sell(trade, order)
|
assert main.handle_timedout_limit_sell(trade, order)
|
||||||
assert cancel_order.call_count == 1
|
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,
|
def test_check_handle_timedout_partial_sell(default_conf, ticker, limit_sell_order_old_partial,
|
||||||
mocker):
|
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)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
cancel_order_mock = MagicMock()
|
cancel_order_mock = MagicMock()
|
||||||
mocker.patch('freqtrade.rpc.init', MagicMock())
|
mocker.patch('freqtrade.rpc.init', MagicMock())
|
||||||
|
Loading…
Reference in New Issue
Block a user