Merge pull request #6235 from freqtrade/backtest_order_timeout
Backtest order timeout
This commit is contained in:
@@ -19,7 +19,7 @@ from freqtrade.edge import PairInfo
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.freqtradebot import FreqtradeBot
|
||||
from freqtrade.persistence import LocalTrade, Trade, init_db
|
||||
from freqtrade.persistence import LocalTrade, Order, Trade, init_db
|
||||
from freqtrade.resolvers import ExchangeResolver
|
||||
from freqtrade.worker import Worker
|
||||
from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, mock_trade_4,
|
||||
@@ -1985,7 +1985,7 @@ def import_fails() -> None:
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def open_trade():
|
||||
return Trade(
|
||||
trade = Trade(
|
||||
pair='ETH/BTC',
|
||||
open_rate=0.00001099,
|
||||
exchange='binance',
|
||||
@@ -1997,6 +1997,26 @@ def open_trade():
|
||||
open_date=arrow.utcnow().shift(minutes=-601).datetime,
|
||||
is_open=True
|
||||
)
|
||||
trade.orders = [
|
||||
Order(
|
||||
ft_order_side='buy',
|
||||
ft_pair=trade.pair,
|
||||
ft_is_open=False,
|
||||
order_id='123456789',
|
||||
status="closed",
|
||||
symbol=trade.pair,
|
||||
order_type="market",
|
||||
side="buy",
|
||||
price=trade.open_rate,
|
||||
average=trade.open_rate,
|
||||
filled=trade.amount,
|
||||
remaining=0,
|
||||
cost=trade.open_rate * trade.amount,
|
||||
order_date=trade.open_date,
|
||||
order_filled_date=trade.open_date,
|
||||
)
|
||||
]
|
||||
return trade
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
|
@@ -36,6 +36,8 @@ class BTContainer(NamedTuple):
|
||||
trailing_stop_positive_offset: float = 0.0
|
||||
use_sell_signal: bool = False
|
||||
use_custom_stoploss: bool = False
|
||||
custom_entry_price: Optional[float] = None
|
||||
custom_exit_price: Optional[float] = None
|
||||
|
||||
|
||||
def _get_frame_time_from_offset(offset):
|
||||
|
@@ -1,5 +1,6 @@
|
||||
# pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, C0330, unused-argument
|
||||
import logging
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -534,6 +535,80 @@ tc33 = BTContainer(data=[
|
||||
)]
|
||||
)
|
||||
|
||||
# Test 34: Custom-entry-price below all candles should timeout - so no trade happens.
|
||||
tc34 = BTContainer(data=[
|
||||
# D O H L C V B S
|
||||
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
|
||||
[1, 5000, 5500, 4951, 5000, 6172, 0, 0], # timeout
|
||||
[2, 4900, 5250, 4500, 5100, 6172, 0, 0],
|
||||
[3, 5100, 5100, 4650, 4750, 6172, 0, 0],
|
||||
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
|
||||
stop_loss=-0.01, roi={"0": 0.10}, profit_perc=0.0,
|
||||
custom_entry_price=4200, trades=[]
|
||||
)
|
||||
|
||||
# Test 35: Custom-entry-price above all candles should have rate adjusted to "entry candle high"
|
||||
tc35 = BTContainer(data=[
|
||||
# D O H L C V B S
|
||||
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
|
||||
[1, 5000, 5500, 4951, 5000, 6172, 0, 0], # Timeout
|
||||
[2, 4900, 5250, 4500, 5100, 6172, 0, 0],
|
||||
[3, 5100, 5100, 4650, 4750, 6172, 0, 0],
|
||||
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
|
||||
stop_loss=-0.01, roi={"0": 0.10}, profit_perc=-0.01,
|
||||
custom_entry_price=7200, trades=[
|
||||
BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=1)
|
||||
]
|
||||
)
|
||||
|
||||
# Test 36: Custom-entry-price around candle low
|
||||
# Causes immediate ROI exit. This is currently expected behavior (#6261)
|
||||
# https://github.com/freqtrade/freqtrade/issues/6261
|
||||
# But may change at a later point.
|
||||
tc36 = BTContainer(data=[
|
||||
# D O H L C V B S BT
|
||||
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
|
||||
[1, 5000, 5500, 4951, 5000, 6172, 0, 0], # Enter and immediate ROI
|
||||
[2, 4900, 5250, 4500, 5100, 6172, 0, 0],
|
||||
[3, 5100, 5100, 4650, 4750, 6172, 0, 0],
|
||||
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
|
||||
stop_loss=-0.01, roi={"0": 0.10}, profit_perc=0.1,
|
||||
custom_entry_price=4952,
|
||||
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=1)]
|
||||
)
|
||||
|
||||
|
||||
# Test 37: Custom exit price below all candles
|
||||
# Price adjusted to candle Low.
|
||||
tc37 = BTContainer(data=[
|
||||
# D O H L C V B S BT
|
||||
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
|
||||
[1, 5000, 5500, 4951, 5000, 6172, 0, 0],
|
||||
[2, 4900, 5250, 4900, 5100, 6172, 0, 1], # exit - but timeout
|
||||
[3, 5100, 5100, 4950, 4950, 6172, 0, 0],
|
||||
[4, 5000, 5100, 4950, 4950, 6172, 0, 0]],
|
||||
stop_loss=-0.10, roi={"0": 0.10}, profit_perc=-0.01,
|
||||
use_sell_signal=True,
|
||||
custom_exit_price=4552,
|
||||
trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=3)]
|
||||
)
|
||||
|
||||
# Test 38: Custom exit price above all candles
|
||||
# causes sell signal timeout
|
||||
tc38 = BTContainer(data=[
|
||||
# D O H L C V B S BT
|
||||
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
|
||||
[1, 5000, 5500, 4951, 5000, 6172, 0, 0],
|
||||
[2, 4900, 5250, 4900, 5100, 6172, 0, 1], # exit - but timeout
|
||||
[3, 5100, 5100, 4950, 4950, 6172, 0, 0],
|
||||
[4, 5000, 5100, 4950, 4950, 6172, 0, 0]],
|
||||
stop_loss=-0.10, roi={"0": 0.10}, profit_perc=0.0,
|
||||
use_sell_signal=True,
|
||||
custom_exit_price=6052,
|
||||
trades=[BTrade(sell_reason=SellType.FORCE_SELL, open_tick=1, close_tick=4)]
|
||||
)
|
||||
|
||||
|
||||
TESTS = [
|
||||
tc0,
|
||||
tc1,
|
||||
@@ -569,6 +644,11 @@ TESTS = [
|
||||
tc31,
|
||||
tc32,
|
||||
tc33,
|
||||
tc34,
|
||||
tc35,
|
||||
tc36,
|
||||
tc37,
|
||||
tc38,
|
||||
]
|
||||
|
||||
|
||||
@@ -597,6 +677,10 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None:
|
||||
backtesting.required_startup = 0
|
||||
backtesting.strategy.advise_buy = lambda a, m: frame
|
||||
backtesting.strategy.advise_sell = lambda a, m: frame
|
||||
if data.custom_entry_price:
|
||||
backtesting.strategy.custom_entry_price = MagicMock(return_value=data.custom_entry_price)
|
||||
if data.custom_exit_price:
|
||||
backtesting.strategy.custom_exit_price = MagicMock(return_value=data.custom_exit_price)
|
||||
backtesting.strategy.use_custom_stoploss = data.use_custom_stoploss
|
||||
caplog.set_level(logging.DEBUG)
|
||||
|
||||
|
@@ -521,6 +521,7 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None:
|
||||
# Fake 2 trades, so there's not enough amount for the next trade left.
|
||||
LocalTrade.trades_open.append(trade)
|
||||
LocalTrade.trades_open.append(trade)
|
||||
backtesting.wallets.update()
|
||||
trade = backtesting._enter_trade(pair, row=row)
|
||||
assert trade is None
|
||||
LocalTrade.trades_open.pop()
|
||||
@@ -528,6 +529,7 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None:
|
||||
assert trade is not None
|
||||
|
||||
backtesting.strategy.custom_stake_amount = lambda **kwargs: 123.5
|
||||
backtesting.wallets.update()
|
||||
trade = backtesting._enter_trade(pair, row=row)
|
||||
assert trade
|
||||
assert trade.stake_amount == 123.5
|
||||
@@ -635,7 +637,8 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None:
|
||||
assert res.sell_reason == SellType.ROI.value
|
||||
# Sell at minute 3 (not available above!)
|
||||
assert res.close_date_utc == datetime(2020, 1, 1, 5, 3, tzinfo=timezone.utc)
|
||||
assert round(res.close_rate, 3) == round(209.0225, 3)
|
||||
sell_order = res.select_order('sell', True)
|
||||
assert sell_order is not None
|
||||
|
||||
|
||||
def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
|
||||
@@ -1020,6 +1023,8 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
|
||||
'config': default_conf,
|
||||
'locks': [],
|
||||
'rejected_signals': 20,
|
||||
'timedout_entry_orders': 0,
|
||||
'timedout_exit_orders': 0,
|
||||
'final_balance': 1000,
|
||||
})
|
||||
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
|
||||
@@ -1128,6 +1133,8 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
|
||||
'config': default_conf,
|
||||
'locks': [],
|
||||
'rejected_signals': 20,
|
||||
'timedout_entry_orders': 0,
|
||||
'timedout_exit_orders': 0,
|
||||
'final_balance': 1000,
|
||||
},
|
||||
{
|
||||
@@ -1135,6 +1142,8 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
|
||||
'config': default_conf,
|
||||
'locks': [],
|
||||
'rejected_signals': 20,
|
||||
'timedout_entry_orders': 0,
|
||||
'timedout_exit_orders': 0,
|
||||
'final_balance': 1000,
|
||||
}
|
||||
])
|
||||
@@ -1237,6 +1246,8 @@ def test_backtest_start_multi_strat_nomock_detail(default_conf, mocker,
|
||||
'config': default_conf,
|
||||
'locks': [],
|
||||
'rejected_signals': 20,
|
||||
'timedout_entry_orders': 0,
|
||||
'timedout_exit_orders': 0,
|
||||
'final_balance': 1000,
|
||||
},
|
||||
{
|
||||
@@ -1244,6 +1255,8 @@ def test_backtest_start_multi_strat_nomock_detail(default_conf, mocker,
|
||||
'config': default_conf,
|
||||
'locks': [],
|
||||
'rejected_signals': 20,
|
||||
'timedout_entry_orders': 0,
|
||||
'timedout_exit_orders': 0,
|
||||
'final_balance': 1000,
|
||||
}
|
||||
])
|
||||
@@ -1305,6 +1318,8 @@ def test_backtest_start_multi_strat_caching(default_conf, mocker, caplog, testda
|
||||
'config': default_conf,
|
||||
'locks': [],
|
||||
'rejected_signals': 20,
|
||||
'timedout_entry_orders': 0,
|
||||
'timedout_exit_orders': 0,
|
||||
'final_balance': 1000,
|
||||
})
|
||||
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
|
||||
|
@@ -364,6 +364,8 @@ def test_hyperopt_format_results(hyperopt):
|
||||
'locks': [],
|
||||
'final_balance': 0.02,
|
||||
'rejected_signals': 2,
|
||||
'timedout_entry_orders': 0,
|
||||
'timedout_exit_orders': 0,
|
||||
'backtest_start_time': 1619718665,
|
||||
'backtest_end_time': 1619718665,
|
||||
}
|
||||
@@ -431,6 +433,8 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
|
||||
'config': hyperopt_conf,
|
||||
'locks': [],
|
||||
'rejected_signals': 20,
|
||||
'timedout_entry_orders': 0,
|
||||
'timedout_exit_orders': 0,
|
||||
'final_balance': 1000,
|
||||
}
|
||||
|
||||
|
@@ -82,6 +82,8 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmpdir):
|
||||
'locks': [],
|
||||
'final_balance': 1000.02,
|
||||
'rejected_signals': 20,
|
||||
'timedout_entry_orders': 0,
|
||||
'timedout_exit_orders': 0,
|
||||
'backtest_start_time': Arrow.utcnow().int_timestamp,
|
||||
'backtest_end_time': Arrow.utcnow().int_timestamp,
|
||||
'run_id': '123',
|
||||
@@ -131,6 +133,8 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmpdir):
|
||||
'locks': [],
|
||||
'final_balance': 1000.02,
|
||||
'rejected_signals': 20,
|
||||
'timedout_entry_orders': 0,
|
||||
'timedout_exit_orders': 0,
|
||||
'backtest_start_time': Arrow.utcnow().int_timestamp,
|
||||
'backtest_end_time': Arrow.utcnow().int_timestamp,
|
||||
'run_id': '124',
|
||||
|
@@ -2042,6 +2042,7 @@ def test_check_handle_timedout_buy_usercustom(default_conf_usdt, ticker_usdt, li
|
||||
def test_check_handle_timedout_buy(default_conf_usdt, ticker_usdt, limit_buy_order_old, open_trade,
|
||||
fee, mocker) -> None:
|
||||
rpc_mock = patch_RPCManager(mocker)
|
||||
limit_buy_order_old['id'] = open_trade.open_order_id
|
||||
limit_buy_cancel = deepcopy(limit_buy_order_old)
|
||||
limit_buy_cancel['status'] = 'canceled'
|
||||
cancel_order_mock = MagicMock(return_value=limit_buy_cancel)
|
||||
@@ -2126,6 +2127,8 @@ def test_check_handle_timedout_buy_exception(default_conf_usdt, ticker_usdt,
|
||||
def test_check_handle_timedout_sell_usercustom(default_conf_usdt, ticker_usdt, limit_sell_order_old,
|
||||
mocker, open_trade, caplog) -> None:
|
||||
default_conf_usdt["unfilledtimeout"] = {"buy": 1440, "sell": 1440, "exit_timeout_count": 1}
|
||||
limit_sell_order_old['id'] = open_trade.open_order_id
|
||||
|
||||
rpc_mock = patch_RPCManager(mocker)
|
||||
cancel_order_mock = MagicMock()
|
||||
patch_exchange(mocker)
|
||||
@@ -2174,7 +2177,7 @@ def test_check_handle_timedout_sell_usercustom(default_conf_usdt, ticker_usdt, l
|
||||
|
||||
# 2nd canceled trade - Fail execute sell
|
||||
caplog.clear()
|
||||
open_trade.open_order_id = 'order_id_2'
|
||||
open_trade.open_order_id = limit_sell_order_old['id']
|
||||
mocker.patch('freqtrade.persistence.Trade.get_exit_order_count', return_value=1)
|
||||
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit',
|
||||
side_effect=DependencyException)
|
||||
@@ -2185,7 +2188,7 @@ def test_check_handle_timedout_sell_usercustom(default_conf_usdt, ticker_usdt, l
|
||||
caplog.clear()
|
||||
|
||||
# 2nd canceled trade ...
|
||||
open_trade.open_order_id = 'order_id_2'
|
||||
open_trade.open_order_id = limit_sell_order_old['id']
|
||||
freqtrade.check_handle_timedout()
|
||||
assert log_has_re('Emergencyselling trade.*', caplog)
|
||||
assert et_mock.call_count == 1
|
||||
@@ -2195,6 +2198,7 @@ def test_check_handle_timedout_sell(default_conf_usdt, ticker_usdt, limit_sell_o
|
||||
open_trade) -> None:
|
||||
rpc_mock = patch_RPCManager(mocker)
|
||||
cancel_order_mock = MagicMock()
|
||||
limit_sell_order_old['id'] = open_trade.open_order_id
|
||||
patch_exchange(mocker)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
@@ -2253,6 +2257,7 @@ def test_check_handle_cancelled_sell(default_conf_usdt, ticker_usdt, limit_sell_
|
||||
def test_check_handle_timedout_partial(default_conf_usdt, ticker_usdt, limit_buy_order_old_partial,
|
||||
open_trade, mocker) -> None:
|
||||
rpc_mock = patch_RPCManager(mocker)
|
||||
limit_buy_order_old_partial['id'] = open_trade.open_order_id
|
||||
limit_buy_canceled = deepcopy(limit_buy_order_old_partial)
|
||||
limit_buy_canceled['status'] = 'canceled'
|
||||
|
||||
@@ -2283,6 +2288,7 @@ def test_check_handle_timedout_partial_fee(default_conf_usdt, ticker_usdt, open_
|
||||
limit_buy_order_old_partial, trades_for_order,
|
||||
limit_buy_order_old_partial_canceled, mocker) -> None:
|
||||
rpc_mock = patch_RPCManager(mocker)
|
||||
limit_buy_order_old_partial['id'] = open_trade.open_order_id
|
||||
cancel_order_mock = MagicMock(return_value=limit_buy_order_old_partial_canceled)
|
||||
mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=0))
|
||||
patch_exchange(mocker)
|
||||
@@ -2322,6 +2328,8 @@ def test_check_handle_timedout_partial_except(default_conf_usdt, ticker_usdt, op
|
||||
fee, limit_buy_order_old_partial, trades_for_order,
|
||||
limit_buy_order_old_partial_canceled, mocker) -> None:
|
||||
rpc_mock = patch_RPCManager(mocker)
|
||||
limit_buy_order_old_partial_canceled['id'] = open_trade.open_order_id
|
||||
limit_buy_order_old_partial['id'] = open_trade.open_order_id
|
||||
cancel_order_mock = MagicMock(return_value=limit_buy_order_old_partial_canceled)
|
||||
patch_exchange(mocker)
|
||||
mocker.patch.multiple(
|
||||
|
Reference in New Issue
Block a user