Merge pull request #1954 from freqtrade/fix/stoploss_cancel_error
Trailing stoploss cancel orders should be handled gracefully
This commit is contained in:
commit
9d2b6db97b
@ -690,13 +690,22 @@ class FreqtradeBot(object):
|
|||||||
# cancelling the current stoploss on exchange first
|
# cancelling the current stoploss on exchange first
|
||||||
logger.info('Trailing stoploss: cancelling current stoploss on exchange (id:{%s})'
|
logger.info('Trailing stoploss: cancelling current stoploss on exchange (id:{%s})'
|
||||||
'in order to add another one ...', order['id'])
|
'in order to add another one ...', order['id'])
|
||||||
if self.exchange.cancel_order(order['id'], trade.pair):
|
try:
|
||||||
|
self.exchange.cancel_order(order['id'], trade.pair)
|
||||||
|
except InvalidOrderException:
|
||||||
|
logger.exception(f"Could not cancel stoploss order {order['id']} "
|
||||||
|
f"for pair {trade.pair}")
|
||||||
|
|
||||||
|
try:
|
||||||
# creating the new one
|
# creating the new one
|
||||||
stoploss_order_id = self.exchange.stoploss_limit(
|
stoploss_order_id = self.exchange.stoploss_limit(
|
||||||
pair=trade.pair, amount=trade.amount,
|
pair=trade.pair, amount=trade.amount,
|
||||||
stop_price=trade.stop_loss, rate=trade.stop_loss * 0.99
|
stop_price=trade.stop_loss, rate=trade.stop_loss * 0.99
|
||||||
)['id']
|
)['id']
|
||||||
trade.stoploss_order_id = str(stoploss_order_id)
|
trade.stoploss_order_id = str(stoploss_order_id)
|
||||||
|
except DependencyException:
|
||||||
|
logger.exception(f"Could create trailing stoploss order "
|
||||||
|
f"for pair {trade.pair}.")
|
||||||
|
|
||||||
def check_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool:
|
def check_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool:
|
||||||
if self.edge:
|
if self.edge:
|
||||||
@ -842,7 +851,10 @@ class FreqtradeBot(object):
|
|||||||
|
|
||||||
# First cancelling stoploss on exchange ...
|
# First cancelling stoploss on exchange ...
|
||||||
if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id:
|
if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id:
|
||||||
|
try:
|
||||||
self.exchange.cancel_order(trade.stoploss_order_id, trade.pair)
|
self.exchange.cancel_order(trade.stoploss_order_id, trade.pair)
|
||||||
|
except InvalidOrderException:
|
||||||
|
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
|
||||||
|
|
||||||
# Execute sell and update trade record
|
# Execute sell and update trade record
|
||||||
order_id = self.exchange.sell(pair=str(trade.pair),
|
order_id = self.exchange.sell(pair=str(trade.pair),
|
||||||
|
@ -110,11 +110,23 @@ def patch_freqtradebot(mocker, config) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
|
def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
|
||||||
|
"""
|
||||||
|
This function patches _init_modules() to not call dependencies
|
||||||
|
:param mocker: a Mocker object to apply patches
|
||||||
|
:param config: Config to pass to the bot
|
||||||
|
:return: FreqtradeBot
|
||||||
|
"""
|
||||||
patch_freqtradebot(mocker, config)
|
patch_freqtradebot(mocker, config)
|
||||||
return FreqtradeBot(config)
|
return FreqtradeBot(config)
|
||||||
|
|
||||||
|
|
||||||
def get_patched_worker(mocker, config) -> Worker:
|
def get_patched_worker(mocker, config) -> Worker:
|
||||||
|
"""
|
||||||
|
This function patches _init_modules() to not call dependencies
|
||||||
|
:param mocker: a Mocker object to apply patches
|
||||||
|
:param config: Config to pass to the bot
|
||||||
|
:return: Worker
|
||||||
|
"""
|
||||||
patch_freqtradebot(mocker, config)
|
patch_freqtradebot(mocker, config)
|
||||||
return Worker(args=None, config=config)
|
return Worker(args=None, config=config)
|
||||||
|
|
||||||
|
@ -19,47 +19,13 @@ from freqtrade.persistence import Trade
|
|||||||
from freqtrade.rpc import RPCMessageType
|
from freqtrade.rpc import RPCMessageType
|
||||||
from freqtrade.state import State
|
from freqtrade.state import State
|
||||||
from freqtrade.strategy.interface import SellCheckTuple, SellType
|
from freqtrade.strategy.interface import SellCheckTuple, SellType
|
||||||
from freqtrade.tests.conftest import (log_has, log_has_re, patch_edge,
|
from freqtrade.tests.conftest import (get_patched_freqtradebot,
|
||||||
patch_exchange, patch_get_signal,
|
get_patched_worker, log_has, log_has_re,
|
||||||
patch_wallet)
|
patch_edge, patch_exchange,
|
||||||
|
patch_get_signal, patch_wallet)
|
||||||
from freqtrade.worker import Worker
|
from freqtrade.worker import Worker
|
||||||
|
|
||||||
|
|
||||||
# Functions for recurrent object patching
|
|
||||||
def patch_freqtradebot(mocker, config) -> None:
|
|
||||||
"""
|
|
||||||
This function patches _init_modules() to not call dependencies
|
|
||||||
:param mocker: a Mocker object to apply patches
|
|
||||||
:param config: Config to pass to the bot
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
|
||||||
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
|
|
||||||
patch_exchange(mocker)
|
|
||||||
|
|
||||||
|
|
||||||
def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
|
|
||||||
"""
|
|
||||||
This function patches _init_modules() to not call dependencies
|
|
||||||
:param mocker: a Mocker object to apply patches
|
|
||||||
:param config: Config to pass to the bot
|
|
||||||
:return: FreqtradeBot
|
|
||||||
"""
|
|
||||||
patch_freqtradebot(mocker, config)
|
|
||||||
return FreqtradeBot(config)
|
|
||||||
|
|
||||||
|
|
||||||
def get_patched_worker(mocker, config) -> Worker:
|
|
||||||
"""
|
|
||||||
This function patches _init_modules() to not call dependencies
|
|
||||||
:param mocker: a Mocker object to apply patches
|
|
||||||
:param config: Config to pass to the bot
|
|
||||||
:return: Worker
|
|
||||||
"""
|
|
||||||
patch_freqtradebot(mocker, config)
|
|
||||||
return Worker(args=None, config=config)
|
|
||||||
|
|
||||||
|
|
||||||
def patch_RPCManager(mocker) -> MagicMock:
|
def patch_RPCManager(mocker) -> MagicMock:
|
||||||
"""
|
"""
|
||||||
This function mock RPC manager to avoid repeating this code in almost every tests
|
This function mock RPC manager to avoid repeating this code in almost every tests
|
||||||
@ -1176,6 +1142,77 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
|
|||||||
stop_price=0.00002344 * 0.95)
|
stop_price=0.00002344 * 0.95)
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, caplog,
|
||||||
|
markets, limit_buy_order,
|
||||||
|
limit_sell_order) -> None:
|
||||||
|
# When trailing stoploss is set
|
||||||
|
stoploss_limit = MagicMock(return_value={'id': 13434334})
|
||||||
|
patch_exchange(mocker)
|
||||||
|
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_ticker=MagicMock(return_value={
|
||||||
|
'bid': 0.00001172,
|
||||||
|
'ask': 0.00001173,
|
||||||
|
'last': 0.00001172
|
||||||
|
}),
|
||||||
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
|
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
||||||
|
get_fee=fee,
|
||||||
|
markets=PropertyMock(return_value=markets),
|
||||||
|
stoploss_limit=stoploss_limit
|
||||||
|
)
|
||||||
|
|
||||||
|
# enabling TSL
|
||||||
|
default_conf['trailing_stop'] = True
|
||||||
|
|
||||||
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
# enabling stoploss on exchange
|
||||||
|
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
||||||
|
|
||||||
|
# setting stoploss
|
||||||
|
freqtrade.strategy.stoploss = -0.05
|
||||||
|
|
||||||
|
# setting stoploss_on_exchange_interval to 60 seconds
|
||||||
|
freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 60
|
||||||
|
patch_get_signal(freqtrade)
|
||||||
|
freqtrade.create_trade()
|
||||||
|
trade = Trade.query.first()
|
||||||
|
trade.is_open = True
|
||||||
|
trade.open_order_id = None
|
||||||
|
trade.stoploss_order_id = "abcd"
|
||||||
|
trade.stop_loss = 0.2
|
||||||
|
trade.stoploss_last_update = arrow.utcnow().shift(minutes=-601).datetime.replace(tzinfo=None)
|
||||||
|
|
||||||
|
stoploss_order_hanging = {
|
||||||
|
'id': "abcd",
|
||||||
|
'status': 'open',
|
||||||
|
'type': 'stop_loss_limit',
|
||||||
|
'price': 3,
|
||||||
|
'average': 2,
|
||||||
|
'info': {
|
||||||
|
'stopPrice': '0.1'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.cancel_order', side_effect=InvalidOrderException())
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hanging)
|
||||||
|
freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
|
||||||
|
assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*",
|
||||||
|
caplog.record_tuples)
|
||||||
|
|
||||||
|
# Still try to create order
|
||||||
|
assert stoploss_limit.call_count == 1
|
||||||
|
|
||||||
|
# Fail creating stoploss order
|
||||||
|
caplog.clear()
|
||||||
|
cancel_mock = mocker.patch("freqtrade.exchange.Exchange.cancel_order", MagicMock())
|
||||||
|
mocker.patch("freqtrade.exchange.Exchange.stoploss_limit", side_effect=DependencyException())
|
||||||
|
freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
|
||||||
|
assert cancel_mock.call_count == 1
|
||||||
|
assert log_has_re(r"Could create trailing stoploss order for pair ETH/BTC\..*",
|
||||||
|
caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
|
def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
|
||||||
markets, limit_buy_order, limit_sell_order) -> None:
|
markets, limit_buy_order, limit_sell_order) -> None:
|
||||||
|
|
||||||
@ -2108,6 +2145,36 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe
|
|||||||
} == last_msg
|
} == last_msg
|
||||||
|
|
||||||
|
|
||||||
|
def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee,
|
||||||
|
markets, caplog) -> None:
|
||||||
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.cancel_order', side_effect=InvalidOrderException())
|
||||||
|
sellmock = MagicMock()
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
_load_markets=MagicMock(return_value={}),
|
||||||
|
get_ticker=ticker,
|
||||||
|
get_fee=fee,
|
||||||
|
markets=PropertyMock(return_value=markets),
|
||||||
|
sell=sellmock
|
||||||
|
)
|
||||||
|
|
||||||
|
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
||||||
|
patch_get_signal(freqtrade)
|
||||||
|
freqtrade.create_trade()
|
||||||
|
|
||||||
|
trade = Trade.query.first()
|
||||||
|
Trade.session = MagicMock()
|
||||||
|
|
||||||
|
freqtrade.config['dry_run'] = False
|
||||||
|
trade.stoploss_order_id = "abcd"
|
||||||
|
|
||||||
|
freqtrade.execute_sell(trade=trade, limit=1234,
|
||||||
|
sell_reason=SellType.STOP_LOSS)
|
||||||
|
assert sellmock.call_count == 1
|
||||||
|
assert log_has('Could not cancel stoploss order abcd', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_execute_sell_with_stoploss_on_exchange(default_conf,
|
def test_execute_sell_with_stoploss_on_exchange(default_conf,
|
||||||
ticker, fee, ticker_sell_up,
|
ticker, fee, ticker_sell_up,
|
||||||
markets, mocker) -> None:
|
markets, mocker) -> None:
|
||||||
|
Loading…
Reference in New Issue
Block a user