Implement partial sell order timeout handling

This commit is contained in:
Ramon Bastiaans 2018-02-04 15:47:09 +01:00
parent 67b4af5ec4
commit 691b4fae6f
4 changed files with 115 additions and 21 deletions

View File

@ -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:

View File

@ -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
)

View File

@ -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 {

View File

@ -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())