Merge pull request #8412 from freqtrade/fix/partial_stops
support partially filled stops
This commit is contained in:
commit
916e1bbc7c
@ -854,7 +854,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
logger.info(f"Canceling stoploss on exchange for {trade}")
|
logger.info(f"Canceling stoploss on exchange for {trade}")
|
||||||
co = self.exchange.cancel_stoploss_order_with_result(
|
co = self.exchange.cancel_stoploss_order_with_result(
|
||||||
trade.stoploss_order_id, trade.pair, trade.amount)
|
trade.stoploss_order_id, trade.pair, trade.amount)
|
||||||
trade.update_order(co)
|
self.update_trade_state(trade, trade.stoploss_order_id, co, stoploss_order=True)
|
||||||
|
|
||||||
# Reset stoploss order id.
|
# Reset stoploss order id.
|
||||||
trade.stoploss_order_id = None
|
trade.stoploss_order_id = None
|
||||||
except InvalidOrderException:
|
except InvalidOrderException:
|
||||||
@ -1172,7 +1173,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
logger.warning('Unable to fetch stoploss order: %s', exception)
|
logger.warning('Unable to fetch stoploss order: %s', exception)
|
||||||
|
|
||||||
if stoploss_order:
|
if stoploss_order:
|
||||||
trade.update_order(stoploss_order)
|
self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order,
|
||||||
|
stoploss_order=True)
|
||||||
|
|
||||||
# We check if stoploss order is fulfilled
|
# We check if stoploss order is fulfilled
|
||||||
if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'):
|
if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'):
|
||||||
|
@ -695,21 +695,24 @@ class LocalTrade():
|
|||||||
else:
|
else:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f'Got different open_order_id {self.open_order_id} != {order.order_id}')
|
f'Got different open_order_id {self.open_order_id} != {order.order_id}')
|
||||||
|
|
||||||
|
elif order.ft_order_side == 'stoploss' and order.status not in ('open', ):
|
||||||
|
self.stoploss_order_id = None
|
||||||
|
self.close_rate_requested = self.stop_loss
|
||||||
|
self.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value
|
||||||
|
if self.is_open:
|
||||||
|
logger.info(f'{order.order_type.upper()} is hit for {self}.')
|
||||||
|
else:
|
||||||
|
raise ValueError(f'Unknown order type: {order.order_type}')
|
||||||
|
|
||||||
|
if order.ft_order_side != self.entry_side:
|
||||||
amount_tr = amount_to_contract_precision(self.amount, self.amount_precision,
|
amount_tr = amount_to_contract_precision(self.amount, self.amount_precision,
|
||||||
self.precision_mode, self.contract_size)
|
self.precision_mode, self.contract_size)
|
||||||
if isclose(order.safe_amount_after_fee, amount_tr, abs_tol=MATH_CLOSE_PREC):
|
if isclose(order.safe_amount_after_fee, amount_tr, abs_tol=MATH_CLOSE_PREC):
|
||||||
self.close(order.safe_price)
|
self.close(order.safe_price)
|
||||||
else:
|
else:
|
||||||
self.recalc_trade_from_orders()
|
self.recalc_trade_from_orders()
|
||||||
elif order.ft_order_side == 'stoploss' and order.status not in ('canceled', 'open'):
|
|
||||||
self.stoploss_order_id = None
|
|
||||||
self.close_rate_requested = self.stop_loss
|
|
||||||
self.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value
|
|
||||||
if self.is_open:
|
|
||||||
logger.info(f'{order.order_type.upper()} is hit for {self}.')
|
|
||||||
self.close(order.safe_price)
|
|
||||||
else:
|
|
||||||
raise ValueError(f'Unknown order type: {order.order_type}')
|
|
||||||
Trade.commit()
|
Trade.commit()
|
||||||
|
|
||||||
def close(self, rate: float, *, show_msg: bool = True) -> None:
|
def close(self, rate: float, *, show_msg: bool = True) -> None:
|
||||||
|
@ -1290,6 +1290,137 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
|
|||||||
assert trade.exit_reason == str(ExitType.EMERGENCY_EXIT)
|
assert trade.exit_reason == str(ExitType.EMERGENCY_EXIT)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("is_short", [False, True])
|
||||||
|
def test_handle_stoploss_on_exchange_partial(
|
||||||
|
mocker, default_conf_usdt, fee, is_short, limit_order) -> None:
|
||||||
|
stop_order_dict = {'id': "101", "status": "open"}
|
||||||
|
stoploss = MagicMock(return_value=stop_order_dict)
|
||||||
|
enter_order = limit_order[entry_side(is_short)]
|
||||||
|
exit_order = limit_order[exit_side(is_short)]
|
||||||
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
EXMS,
|
||||||
|
fetch_ticker=MagicMock(return_value={
|
||||||
|
'bid': 1.9,
|
||||||
|
'ask': 2.2,
|
||||||
|
'last': 1.9
|
||||||
|
}),
|
||||||
|
create_order=MagicMock(side_effect=[
|
||||||
|
enter_order,
|
||||||
|
exit_order,
|
||||||
|
]),
|
||||||
|
get_fee=fee,
|
||||||
|
create_stoploss=stoploss
|
||||||
|
)
|
||||||
|
freqtrade = FreqtradeBot(default_conf_usdt)
|
||||||
|
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
|
||||||
|
|
||||||
|
freqtrade.enter_positions()
|
||||||
|
trade = Trade.session.scalars(select(Trade)).first()
|
||||||
|
trade.is_short = is_short
|
||||||
|
trade.is_open = True
|
||||||
|
trade.open_order_id = None
|
||||||
|
trade.stoploss_order_id = None
|
||||||
|
|
||||||
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||||
|
assert stoploss.call_count == 1
|
||||||
|
assert trade.stoploss_order_id == "101"
|
||||||
|
assert trade.amount == 30
|
||||||
|
stop_order_dict.update({'id': "102"})
|
||||||
|
# Stoploss on exchange is cancelled on exchange, but filled partially.
|
||||||
|
# Must update trade amount to guarantee successful exit.
|
||||||
|
stoploss_order_hit = MagicMock(return_value={
|
||||||
|
'id': "101",
|
||||||
|
'status': 'canceled',
|
||||||
|
'type': 'stop_loss_limit',
|
||||||
|
'price': 3,
|
||||||
|
'average': 2,
|
||||||
|
'filled': trade.amount / 2,
|
||||||
|
'remaining': trade.amount / 2,
|
||||||
|
'amount': enter_order['amount'],
|
||||||
|
})
|
||||||
|
mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hit)
|
||||||
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||||
|
# Stoploss filled partially ...
|
||||||
|
assert trade.amount == 15
|
||||||
|
|
||||||
|
assert trade.stoploss_order_id == "102"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("is_short", [False, True])
|
||||||
|
def test_handle_stoploss_on_exchange_partial_cancel_here(
|
||||||
|
mocker, default_conf_usdt, fee, is_short, limit_order, caplog) -> None:
|
||||||
|
stop_order_dict = {'id': "101", "status": "open"}
|
||||||
|
default_conf_usdt['trailing_stop'] = True
|
||||||
|
stoploss = MagicMock(return_value=stop_order_dict)
|
||||||
|
enter_order = limit_order[entry_side(is_short)]
|
||||||
|
exit_order = limit_order[exit_side(is_short)]
|
||||||
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
EXMS,
|
||||||
|
fetch_ticker=MagicMock(return_value={
|
||||||
|
'bid': 1.9,
|
||||||
|
'ask': 2.2,
|
||||||
|
'last': 1.9
|
||||||
|
}),
|
||||||
|
create_order=MagicMock(side_effect=[
|
||||||
|
enter_order,
|
||||||
|
exit_order,
|
||||||
|
]),
|
||||||
|
get_fee=fee,
|
||||||
|
create_stoploss=stoploss
|
||||||
|
)
|
||||||
|
freqtrade = FreqtradeBot(default_conf_usdt)
|
||||||
|
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
|
||||||
|
|
||||||
|
freqtrade.enter_positions()
|
||||||
|
trade = Trade.session.scalars(select(Trade)).first()
|
||||||
|
trade.is_short = is_short
|
||||||
|
trade.is_open = True
|
||||||
|
trade.open_order_id = None
|
||||||
|
trade.stoploss_order_id = None
|
||||||
|
|
||||||
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||||
|
assert stoploss.call_count == 1
|
||||||
|
assert trade.stoploss_order_id == "101"
|
||||||
|
assert trade.amount == 30
|
||||||
|
stop_order_dict.update({'id': "102"})
|
||||||
|
# Stoploss on exchange is open.
|
||||||
|
# Freqtrade cancels the stop - but cancel returns a partial filled order.
|
||||||
|
stoploss_order_hit = MagicMock(return_value={
|
||||||
|
'id': "101",
|
||||||
|
'status': 'open',
|
||||||
|
'type': 'stop_loss_limit',
|
||||||
|
'price': 3,
|
||||||
|
'average': 2,
|
||||||
|
'filled': 0,
|
||||||
|
'remaining': trade.amount,
|
||||||
|
'amount': enter_order['amount'],
|
||||||
|
})
|
||||||
|
stoploss_order_cancel = MagicMock(return_value={
|
||||||
|
'id': "101",
|
||||||
|
'status': 'canceled',
|
||||||
|
'type': 'stop_loss_limit',
|
||||||
|
'price': 3,
|
||||||
|
'average': 2,
|
||||||
|
'filled': trade.amount / 2,
|
||||||
|
'remaining': trade.amount / 2,
|
||||||
|
'amount': enter_order['amount'],
|
||||||
|
})
|
||||||
|
mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hit)
|
||||||
|
mocker.patch(f'{EXMS}.cancel_stoploss_order_with_result', stoploss_order_cancel)
|
||||||
|
trade.stoploss_last_update = arrow.utcnow().shift(minutes=-10).datetime
|
||||||
|
|
||||||
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||||
|
# Canceled Stoploss filled partially ...
|
||||||
|
assert log_has_re('Cancelling current stoploss on exchange.*', caplog)
|
||||||
|
|
||||||
|
assert trade.stoploss_order_id == "102"
|
||||||
|
assert trade.amount == 15
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("is_short", [False, True])
|
@pytest.mark.parametrize("is_short", [False, True])
|
||||||
def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog, is_short,
|
def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog, is_short,
|
||||||
limit_order) -> None:
|
limit_order) -> None:
|
||||||
|
Loading…
Reference in New Issue
Block a user