Merge pull request #3146 from freqtrade/buy_order_timeout_logging

Improve handling for buy order cancels
This commit is contained in:
hroff-1902 2020-04-16 23:41:45 +03:00 committed by GitHub
commit 68be239a0e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 58 additions and 12 deletions

View File

@ -20,6 +20,7 @@ from freqtrade.data.dataprovider import DataProvider
from freqtrade.edge import Edge from freqtrade.edge import Edge
from freqtrade.exceptions import DependencyException, InvalidOrderException from freqtrade.exceptions import DependencyException, InvalidOrderException
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date
from freqtrade.misc import safe_value_fallback
from freqtrade.pairlist.pairlistmanager import PairListManager from freqtrade.pairlist.pairlistmanager import PairListManager
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.resolvers import ExchangeResolver, StrategyResolver
@ -889,18 +890,24 @@ class FreqtradeBot:
""" """
if order['status'] != 'canceled': if order['status'] != 'canceled':
reason = "cancelled due to timeout" reason = "cancelled due to timeout"
corder = self.exchange.cancel_order(trade.open_order_id, trade.pair) try:
# Some exchanges don't return a dict here. corder = self.exchange.cancel_order(trade.open_order_id, trade.pair)
if not isinstance(corder, dict): # Some exchanges don't return a dict here.
if not isinstance(corder, dict):
corder = {}
logger.info('Buy order %s for %s.', reason, trade)
except InvalidOrderException:
corder = {} corder = {}
logger.info('Buy order %s for %s.', reason, trade) logger.exception(
f"Could not cancel buy order {trade.open_order_id} for pair {trade.pair}")
else: else:
# Order was cancelled already, so we can reuse the existing dict # Order was cancelled already, so we can reuse the existing dict
corder = order corder = order
reason = "cancelled on exchange" reason = "cancelled on exchange"
logger.info('Buy order %s for %s.', reason, trade) logger.info('Buy order %s for %s.', reason, trade)
if corder.get('remaining', order['remaining']) == order['amount']: if safe_value_fallback(corder, order, 'remaining', 'remaining') == order['amount']:
logger.info('Buy order fully cancelled. Removing %s from database.', trade)
# if trade is not partially completed, just delete the trade # if trade is not partially completed, just delete the trade
Trade.session.delete(trade) Trade.session.delete(trade)
Trade.session.flush() Trade.session.flush()
@ -911,7 +918,8 @@ class FreqtradeBot:
# cancel_order may not contain the full order dict, so we need to fallback # cancel_order may not contain the full order dict, so we need to fallback
# to the order dict aquired before cancelling. # to the order dict aquired before cancelling.
# we need to fall back to the values from order if corder does not contain these keys. # we need to fall back to the values from order if corder does not contain these keys.
trade.amount = order['amount'] - corder.get('remaining', order['remaining']) trade.amount = order['amount'] - safe_value_fallback(corder, order,
'remaining', 'remaining')
trade.stake_amount = trade.amount * trade.open_rate trade.stake_amount = trade.amount * trade.open_rate
self.update_trade_state(trade, corder if 'fee' in corder else order, trade.amount) self.update_trade_state(trade, corder if 'fee' in corder else order, trade.amount)

View File

@ -134,6 +134,21 @@ def round_dict(d, n):
return {k: (round(v, n) if isinstance(v, float) else v) for k, v in d.items()} return {k: (round(v, n) if isinstance(v, float) else v) for k, v in d.items()}
def safe_value_fallback(dict1: dict, dict2: dict, key1: str, key2: str, default_value=None):
"""
Search a value in dict1, return this if it's not None.
Fall back to dict2 - return key2 from dict2 if it's not None.
Else falls back to None.
"""
if key1 in dict1 and dict1[key1] is not None:
return dict1[key1]
else:
if key2 in dict2 and dict2[key2] is not None:
return dict2[key2]
return default_value
def plural(num: float, singular: str, plural: str = None) -> str: def plural(num: float, singular: str, plural: str = None) -> str:
return singular if (num == 1 or num == -1) else plural or singular + 's' return singular if (num == 1 or num == -1) else plural or singular + 's'

View File

@ -2204,14 +2204,11 @@ def test_check_handle_timedout_exception(default_conf, ticker, open_trade, mocke
caplog) caplog)
def test_handle_timedout_limit_buy(mocker, default_conf, limit_buy_order) -> None: def test_handle_timedout_limit_buy(mocker, caplog, default_conf, limit_buy_order) -> None:
patch_RPCManager(mocker) patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
cancel_order_mock = MagicMock(return_value=limit_buy_order) cancel_order_mock = MagicMock(return_value=limit_buy_order)
mocker.patch.multiple( mocker.patch('freqtrade.exchange.Exchange.cancel_order', cancel_order_mock)
'freqtrade.exchange.Exchange',
cancel_order=cancel_order_mock
)
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
@ -2227,9 +2224,14 @@ def test_handle_timedout_limit_buy(mocker, default_conf, limit_buy_order) -> Non
assert not freqtrade.handle_timedout_limit_buy(trade, limit_buy_order) assert not freqtrade.handle_timedout_limit_buy(trade, limit_buy_order)
assert cancel_order_mock.call_count == 1 assert cancel_order_mock.call_count == 1
mocker.patch('freqtrade.exchange.Exchange.cancel_order', side_effect=InvalidOrderException)
assert not freqtrade.handle_timedout_limit_buy(trade, limit_buy_order)
assert log_has_re(r"Could not cancel buy order", caplog)
@pytest.mark.parametrize('cancelorder', [ @pytest.mark.parametrize('cancelorder', [
{}, {},
{'remaining': None},
'String Return value', 'String Return value',
123 123
]) ])

View File

@ -9,7 +9,7 @@ import pytest
from freqtrade.data.converter import ohlcv_to_dataframe from freqtrade.data.converter import ohlcv_to_dataframe
from freqtrade.misc import (datesarray_to_datetimearray, file_dump_json, from freqtrade.misc import (datesarray_to_datetimearray, file_dump_json,
file_load_json, format_ms_time, pair_to_filename, file_load_json, format_ms_time, pair_to_filename,
plural, shorten_date) plural, safe_value_fallback, shorten_date)
def test_shorten_date() -> None: def test_shorten_date() -> None:
@ -93,6 +93,27 @@ def test_format_ms_time() -> None:
assert format_ms_time(date_in_epoch_ms) == res.astimezone(None).strftime('%Y-%m-%dT%H:%M:%S') assert format_ms_time(date_in_epoch_ms) == res.astimezone(None).strftime('%Y-%m-%dT%H:%M:%S')
def test_safe_value_fallback():
dict1 = {'keya': None, 'keyb': 2, 'keyc': 5, 'keyd': None}
dict2 = {'keya': 20, 'keyb': None, 'keyc': 6, 'keyd': None}
assert safe_value_fallback(dict1, dict2, 'keya', 'keya') == 20
assert safe_value_fallback(dict2, dict1, 'keya', 'keya') == 20
assert safe_value_fallback(dict1, dict2, 'keyb', 'keyb') == 2
assert safe_value_fallback(dict2, dict1, 'keyb', 'keyb') == 2
assert safe_value_fallback(dict1, dict2, 'keyc', 'keyc') == 5
assert safe_value_fallback(dict2, dict1, 'keyc', 'keyc') == 6
assert safe_value_fallback(dict1, dict2, 'keyd', 'keyd') is None
assert safe_value_fallback(dict2, dict1, 'keyd', 'keyd') is None
assert safe_value_fallback(dict2, dict1, 'keyd', 'keyd', 1234) == 1234
assert safe_value_fallback(dict1, dict2, 'keyNo', 'keyNo') is None
assert safe_value_fallback(dict2, dict1, 'keyNo', 'keyNo') is None
assert safe_value_fallback(dict2, dict1, 'keyNo', 'keyNo', 1234) == 1234
def test_plural() -> None: def test_plural() -> None:
assert plural(0, "page") == "pages" assert plural(0, "page") == "pages"
assert plural(0.0, "page") == "pages" assert plural(0.0, "page") == "pages"