Merge pull request #2872 from freqtrade/interface_ordertimeoutcallback
Buy order timeout callback
This commit is contained in:
@@ -9,10 +9,11 @@ from pandas import DataFrame
|
||||
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.data.history import load_data
|
||||
from freqtrade.exceptions import DependencyException
|
||||
from freqtrade.exceptions import StrategyError
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.resolvers import StrategyResolver
|
||||
from tests.conftest import get_patched_exchange, log_has
|
||||
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||
from tests.conftest import get_patched_exchange, log_has, log_has_re
|
||||
|
||||
from .strats.default_strategy import DefaultStrategy
|
||||
|
||||
@@ -71,7 +72,7 @@ def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ohlcv_his
|
||||
)
|
||||
assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'],
|
||||
ohlcv_history)
|
||||
assert log_has('Unable to analyze candle (OHLCV) data for pair foo: xyz', caplog)
|
||||
assert log_has_re(r'Strategy caused the following exception: xyz.*', caplog)
|
||||
|
||||
|
||||
def test_get_signal_empty_dataframe(default_conf, mocker, caplog, ohlcv_history):
|
||||
@@ -121,7 +122,7 @@ def test_assert_df_raise(default_conf, mocker, caplog, ohlcv_history):
|
||||
caplog.set_level(logging.INFO)
|
||||
mocker.patch.object(
|
||||
_STRATEGY, 'assert_df',
|
||||
side_effect=DependencyException('Dataframe returned...')
|
||||
side_effect=StrategyError('Dataframe returned...')
|
||||
)
|
||||
assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'],
|
||||
ohlcv_history)
|
||||
@@ -134,15 +135,15 @@ def test_assert_df(default_conf, mocker, ohlcv_history):
|
||||
_STRATEGY.assert_df(ohlcv_history, len(ohlcv_history),
|
||||
ohlcv_history.loc[1, 'close'], ohlcv_history.loc[1, 'date'])
|
||||
|
||||
with pytest.raises(DependencyException, match=r"Dataframe returned from strategy.*length\."):
|
||||
with pytest.raises(StrategyError, match=r"Dataframe returned from strategy.*length\."):
|
||||
_STRATEGY.assert_df(ohlcv_history, len(ohlcv_history) + 1,
|
||||
ohlcv_history.loc[1, 'close'], ohlcv_history.loc[1, 'date'])
|
||||
|
||||
with pytest.raises(DependencyException,
|
||||
with pytest.raises(StrategyError,
|
||||
match=r"Dataframe returned from strategy.*last close price\."):
|
||||
_STRATEGY.assert_df(ohlcv_history, len(ohlcv_history),
|
||||
ohlcv_history.loc[1, 'close'] + 0.01, ohlcv_history.loc[1, 'date'])
|
||||
with pytest.raises(DependencyException,
|
||||
with pytest.raises(StrategyError,
|
||||
match=r"Dataframe returned from strategy.*last date\."):
|
||||
_STRATEGY.assert_df(ohlcv_history, len(ohlcv_history),
|
||||
ohlcv_history.loc[1, 'close'], ohlcv_history.loc[0, 'date'])
|
||||
@@ -389,3 +390,38 @@ def test_is_pair_locked(default_conf):
|
||||
pair = 'ETH/BTC'
|
||||
strategy.unlock_pair(pair)
|
||||
assert not strategy.is_pair_locked(pair)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('error', [
|
||||
ValueError, KeyError, Exception,
|
||||
])
|
||||
def test_strategy_safe_wrapper_error(caplog, error):
|
||||
def failing_method():
|
||||
raise error('This is an error.')
|
||||
|
||||
def working_method(argumentpassedin):
|
||||
return argumentpassedin
|
||||
|
||||
with pytest.raises(StrategyError, match=r'This is an error.'):
|
||||
strategy_safe_wrapper(failing_method, message='DeadBeef')()
|
||||
|
||||
assert log_has_re(r'DeadBeef.*', caplog)
|
||||
ret = strategy_safe_wrapper(failing_method, message='DeadBeef', default_retval=True)()
|
||||
|
||||
assert isinstance(ret, bool)
|
||||
assert ret
|
||||
|
||||
|
||||
@pytest.mark.parametrize('value', [
|
||||
1, 22, 55, True, False, {'a': 1, 'b': '112'},
|
||||
[1, 2, 3, 4], (4, 2, 3, 6)
|
||||
])
|
||||
def test_strategy_safe_wrapper(value):
|
||||
|
||||
def working_method(argumentpassedin):
|
||||
return argumentpassedin
|
||||
|
||||
ret = strategy_safe_wrapper(working_method, message='DeadBeef')(value)
|
||||
|
||||
assert type(ret) == type(value)
|
||||
assert ret == value
|
||||
|
@@ -1939,6 +1939,53 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order,
|
||||
freqtrade.handle_trade(trade)
|
||||
|
||||
|
||||
def test_check_handle_timedout_buy_usercustom(default_conf, ticker, limit_buy_order_old, open_trade,
|
||||
fee, mocker) -> None:
|
||||
default_conf["unfilledtimeout"] = {"buy": 1400, "sell": 30}
|
||||
|
||||
rpc_mock = patch_RPCManager(mocker)
|
||||
cancel_order_mock = MagicMock(return_value=limit_buy_order_old)
|
||||
patch_exchange(mocker)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
fetch_ticker=ticker,
|
||||
get_order=MagicMock(return_value=limit_buy_order_old),
|
||||
cancel_order=cancel_order_mock,
|
||||
get_fee=fee
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
|
||||
Trade.session.add(open_trade)
|
||||
|
||||
# Return false - trade remains open
|
||||
freqtrade.strategy.check_buy_timeout = MagicMock(return_value=False)
|
||||
freqtrade.check_handle_timedout()
|
||||
assert cancel_order_mock.call_count == 0
|
||||
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
|
||||
nb_trades = len(trades)
|
||||
assert nb_trades == 1
|
||||
assert freqtrade.strategy.check_buy_timeout.call_count == 1
|
||||
|
||||
# Raise Keyerror ... (no impact on trade)
|
||||
freqtrade.strategy.check_buy_timeout = MagicMock(side_effect=KeyError)
|
||||
freqtrade.check_handle_timedout()
|
||||
assert cancel_order_mock.call_count == 0
|
||||
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
|
||||
nb_trades = len(trades)
|
||||
assert nb_trades == 1
|
||||
assert freqtrade.strategy.check_buy_timeout.call_count == 1
|
||||
|
||||
freqtrade.strategy.check_buy_timeout = MagicMock(return_value=True)
|
||||
# Trade should be closed since the function returns true
|
||||
freqtrade.check_handle_timedout()
|
||||
assert cancel_order_mock.call_count == 1
|
||||
assert rpc_mock.call_count == 1
|
||||
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
|
||||
nb_trades = len(trades)
|
||||
assert nb_trades == 0
|
||||
assert freqtrade.strategy.check_buy_timeout.call_count == 1
|
||||
|
||||
|
||||
def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, open_trade,
|
||||
fee, mocker) -> None:
|
||||
rpc_mock = patch_RPCManager(mocker)
|
||||
@@ -1955,6 +2002,7 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, op
|
||||
|
||||
Trade.session.add(open_trade)
|
||||
|
||||
freqtrade.strategy.check_buy_timeout = MagicMock(return_value=False)
|
||||
# check it does cancel buy orders over the time limit
|
||||
freqtrade.check_handle_timedout()
|
||||
assert cancel_order_mock.call_count == 1
|
||||
@@ -1962,6 +2010,8 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, op
|
||||
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
|
||||
nb_trades = len(trades)
|
||||
assert nb_trades == 0
|
||||
# Custom user buy-timeout is never called
|
||||
assert freqtrade.strategy.check_buy_timeout.call_count == 0
|
||||
|
||||
|
||||
def test_check_handle_cancelled_buy(default_conf, ticker, limit_buy_order_old, open_trade,
|
||||
@@ -2018,6 +2068,51 @@ def test_check_handle_timedout_buy_exception(default_conf, ticker, limit_buy_ord
|
||||
assert nb_trades == 1
|
||||
|
||||
|
||||
def test_check_handle_timedout_sell_usercustom(default_conf, ticker, limit_sell_order_old, mocker,
|
||||
open_trade) -> None:
|
||||
default_conf["unfilledtimeout"] = {"buy": 1440, "sell": 1440}
|
||||
rpc_mock = patch_RPCManager(mocker)
|
||||
cancel_order_mock = MagicMock()
|
||||
patch_exchange(mocker)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
fetch_ticker=ticker,
|
||||
get_order=MagicMock(return_value=limit_sell_order_old),
|
||||
cancel_order=cancel_order_mock
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
|
||||
open_trade.open_date = arrow.utcnow().shift(hours=-5).datetime
|
||||
open_trade.close_date = arrow.utcnow().shift(minutes=-601).datetime
|
||||
open_trade.is_open = False
|
||||
|
||||
Trade.session.add(open_trade)
|
||||
|
||||
freqtrade.strategy.check_sell_timeout = MagicMock(return_value=False)
|
||||
# Return false - No impact
|
||||
freqtrade.check_handle_timedout()
|
||||
assert cancel_order_mock.call_count == 0
|
||||
assert rpc_mock.call_count == 0
|
||||
assert open_trade.is_open is False
|
||||
assert freqtrade.strategy.check_sell_timeout.call_count == 1
|
||||
|
||||
freqtrade.strategy.check_sell_timeout = MagicMock(side_effect=KeyError)
|
||||
# Return Error - No impact
|
||||
freqtrade.check_handle_timedout()
|
||||
assert cancel_order_mock.call_count == 0
|
||||
assert rpc_mock.call_count == 0
|
||||
assert open_trade.is_open is False
|
||||
assert freqtrade.strategy.check_sell_timeout.call_count == 1
|
||||
|
||||
# Return True - sells!
|
||||
freqtrade.strategy.check_sell_timeout = MagicMock(return_value=True)
|
||||
freqtrade.check_handle_timedout()
|
||||
assert cancel_order_mock.call_count == 1
|
||||
assert rpc_mock.call_count == 1
|
||||
assert open_trade.is_open is True
|
||||
assert freqtrade.strategy.check_sell_timeout.call_count == 1
|
||||
|
||||
|
||||
def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, mocker,
|
||||
open_trade) -> None:
|
||||
rpc_mock = patch_RPCManager(mocker)
|
||||
@@ -2037,11 +2132,14 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old,
|
||||
|
||||
Trade.session.add(open_trade)
|
||||
|
||||
freqtrade.strategy.check_sell_timeout = MagicMock(return_value=False)
|
||||
# check it does cancel sell orders over the time limit
|
||||
freqtrade.check_handle_timedout()
|
||||
assert cancel_order_mock.call_count == 1
|
||||
assert rpc_mock.call_count == 1
|
||||
assert open_trade.is_open is True
|
||||
# Custom user sell-timeout is never called
|
||||
assert freqtrade.strategy.check_sell_timeout.call_count == 0
|
||||
|
||||
|
||||
def test_check_handle_cancelled_sell(default_conf, ticker, limit_sell_order_old, open_trade,
|
||||
|
@@ -9,7 +9,9 @@ import pytest
|
||||
from freqtrade.data.converter import ohlcv_to_dataframe
|
||||
from freqtrade.misc import (datesarray_to_datetimearray, file_dump_json,
|
||||
file_load_json, format_ms_time, pair_to_filename,
|
||||
plural, safe_value_fallback, shorten_date)
|
||||
plural, render_template,
|
||||
render_template_with_fallback, safe_value_fallback,
|
||||
shorten_date)
|
||||
|
||||
|
||||
def test_shorten_date() -> None:
|
||||
@@ -144,3 +146,17 @@ def test_plural() -> None:
|
||||
assert plural(1.5, "ox", "oxen") == "oxen"
|
||||
assert plural(-0.5, "ox", "oxen") == "oxen"
|
||||
assert plural(-1.5, "ox", "oxen") == "oxen"
|
||||
|
||||
|
||||
def test_render_template_fallback(mocker):
|
||||
from jinja2.exceptions import TemplateNotFound
|
||||
with pytest.raises(TemplateNotFound):
|
||||
val = render_template(
|
||||
templatefile='subtemplates/indicators_does-not-exist.j2',)
|
||||
|
||||
val = render_template_with_fallback(
|
||||
templatefile='subtemplates/indicators_does-not-exist.j2',
|
||||
templatefallbackfile='subtemplates/indicators_minimal.j2',
|
||||
)
|
||||
assert isinstance(val, str)
|
||||
assert 'if self.dp' in val
|
||||
|
Reference in New Issue
Block a user