Merge pull request #10062 from Axel-CH/feature/proceed-exit-while-open-order
Feature: Proceed exit while having open order, for backtesting and live
This commit is contained in:
@@ -988,6 +988,29 @@ def get_markets():
|
||||
},
|
||||
"info": {},
|
||||
},
|
||||
"ETC/BTC": {
|
||||
"id": "ETCBTC",
|
||||
"symbol": "ETC/BTC",
|
||||
"base": "ETC",
|
||||
"quote": "BTC",
|
||||
"active": True,
|
||||
"spot": True,
|
||||
"swap": False,
|
||||
"linear": None,
|
||||
"type": "spot",
|
||||
"contractSize": None,
|
||||
"precision": {"base": 8, "quote": 8, "amount": 2, "price": 7},
|
||||
"limits": {
|
||||
"amount": {"min": 0.01, "max": 90000000.0},
|
||||
"price": {"min": 1e-07, "max": 1000.0},
|
||||
"cost": {"min": 0.0001, "max": 9000000.0},
|
||||
"leverage": {
|
||||
"min": None,
|
||||
"max": None,
|
||||
},
|
||||
},
|
||||
"info": {},
|
||||
},
|
||||
"ETH/USDT": {
|
||||
"id": "USDT-ETH",
|
||||
"symbol": "ETH/USDT",
|
||||
|
||||
@@ -1257,7 +1257,7 @@ def test_enter_positions(
|
||||
def test_exit_positions(mocker, default_conf_usdt, limit_order, is_short, caplog) -> None:
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
|
||||
mocker.patch("freqtrade.freqtradebot.FreqtradeBot.handle_trade", MagicMock(return_value=True))
|
||||
mocker.patch("freqtrade.freqtradebot.FreqtradeBot.handle_trade", MagicMock(return_value=False))
|
||||
mocker.patch(f"{EXMS}.fetch_order", return_value=limit_order[entry_side(is_short)])
|
||||
mocker.patch(f"{EXMS}.get_trades_for_order", return_value=[])
|
||||
|
||||
@@ -1329,6 +1329,7 @@ def test_exit_positions_exception(mocker, default_conf_usdt, limit_order, caplog
|
||||
ft_price=trade.open_rate,
|
||||
order_id=order_id,
|
||||
ft_is_open=False,
|
||||
filled=11,
|
||||
)
|
||||
)
|
||||
Trade.session.add(trade)
|
||||
@@ -5957,13 +5958,13 @@ def test_check_and_call_adjust_trade_position(mocker, default_conf_usdt, fee, ca
|
||||
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=(10, "aaaa"))
|
||||
freqtrade.process_open_trade_positions()
|
||||
assert log_has_re(r"Max adjustment entries for .* has been reached\.", caplog)
|
||||
assert freqtrade.strategy.adjust_trade_position.call_count == 1
|
||||
assert freqtrade.strategy.adjust_trade_position.call_count == 4
|
||||
|
||||
caplog.clear()
|
||||
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=(-0.0005, "partial_exit_c"))
|
||||
freqtrade.process_open_trade_positions()
|
||||
assert log_has_re(r"LIMIT_SELL has been fulfilled.*", caplog)
|
||||
assert freqtrade.strategy.adjust_trade_position.call_count == 1
|
||||
assert freqtrade.strategy.adjust_trade_position.call_count == 4
|
||||
trade = Trade.get_trades(trade_filter=[Trade.id == 5]).first()
|
||||
assert trade.orders[-1].ft_order_tag == "partial_exit_c"
|
||||
assert trade.is_open
|
||||
|
||||
@@ -436,7 +436,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
|
||||
|
||||
# Replace new order with diff. order at a lower price
|
||||
freqtrade.strategy.adjust_entry_price = MagicMock(return_value=1.95)
|
||||
|
||||
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=None)
|
||||
freqtrade.process()
|
||||
trade = Trade.get_trades().first()
|
||||
assert len(trade.orders) == 4
|
||||
@@ -478,10 +478,14 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
|
||||
assert pytest.approx(trade.amount) == 91.689215 * leverage
|
||||
assert pytest.approx(trade.orders[-1].amount) == 91.689215 * leverage
|
||||
assert freqtrade.strategy.adjust_entry_price.call_count == 0
|
||||
|
||||
# Process again, should not adjust entry price
|
||||
freqtrade.process()
|
||||
trade = Trade.get_trades().first()
|
||||
|
||||
assert trade.orders[-2].status == "closed"
|
||||
assert len(trade.orders) == 5
|
||||
assert trade.orders[-1].side == trade.exit_side
|
||||
assert trade.orders[-1].status == "open"
|
||||
assert trade.orders[-1].price == 2.02
|
||||
# Adjust entry price cannot be called - this is an exit order
|
||||
@@ -532,7 +536,7 @@ def test_dca_order_adjust_entry_replace_fails(
|
||||
|
||||
freqtrade.process()
|
||||
|
||||
assert freqtrade.strategy.adjust_trade_position.call_count == 1
|
||||
assert freqtrade.strategy.adjust_trade_position.call_count == 2
|
||||
trades = Trade.session.scalars(
|
||||
select(Trade)
|
||||
.where(Order.ft_is_open.is_(True))
|
||||
@@ -677,7 +681,11 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, levera
|
||||
assert trade.orders[-1].ft_order_side == "sell"
|
||||
assert pytest.approx(trade.stake_amount) == 40
|
||||
assert trade.is_open is False
|
||||
assert log_has_re("Amount to exit is 0.0 due to exchange limits - not exiting.", caplog)
|
||||
assert log_has_re(
|
||||
"Wanted to exit of -0.01 amount, but exit amount is now 0.0 due to exchange limits "
|
||||
"- not exiting.",
|
||||
caplog,
|
||||
)
|
||||
expected_profit = starting_amount - 60 + trade.realized_profit
|
||||
assert pytest.approx(freqtrade.wallets.get_free("USDT")) == expected_profit
|
||||
if spot:
|
||||
@@ -685,3 +693,133 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, levera
|
||||
else:
|
||||
# total won't change in futures mode, only free / used will.
|
||||
assert freqtrade.wallets.get_total("USDT") == starting_amount + trade.realized_profit
|
||||
|
||||
|
||||
@pytest.mark.parametrize("leverage", [1, 2])
|
||||
@pytest.mark.parametrize("is_short", [False, True])
|
||||
def test_dca_handle_similar_open_order(
|
||||
default_conf_usdt, ticker_usdt, is_short, leverage, fee, mocker, caplog
|
||||
) -> None:
|
||||
default_conf_usdt["position_adjustment_enable"] = True
|
||||
default_conf_usdt["trading_mode"] = "futures"
|
||||
default_conf_usdt["margin_mode"] = "isolated"
|
||||
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
fetch_ticker=ticker_usdt,
|
||||
get_fee=fee,
|
||||
amount_to_precision=lambda s, x, y: y,
|
||||
price_to_precision=lambda s, x, y: y,
|
||||
)
|
||||
mocker.patch(f"{EXMS}._dry_is_price_crossed", return_value=False)
|
||||
mocker.patch(f"{EXMS}.get_max_leverage", return_value=10)
|
||||
mocker.patch(f"{EXMS}.get_funding_fees", return_value=0)
|
||||
mocker.patch(f"{EXMS}.get_maintenance_ratio_and_amt", return_value=(0, 0))
|
||||
|
||||
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
|
||||
freqtrade.strategy.custom_entry_price = lambda **kwargs: ticker_usdt["ask"] * 0.96
|
||||
freqtrade.strategy.leverage = MagicMock(return_value=leverage)
|
||||
freqtrade.strategy.custom_exit = MagicMock(return_value=False)
|
||||
freqtrade.strategy.minimal_roi = {0: 0.2}
|
||||
|
||||
# Create trade and initial entry order
|
||||
freqtrade.enter_positions()
|
||||
|
||||
assert len(Trade.get_trades().all()) == 1
|
||||
trade: Trade = Trade.get_trades().first()
|
||||
assert len(trade.orders) == 1
|
||||
assert trade.orders[-1].side == trade.entry_side
|
||||
assert trade.orders[-1].status == "open"
|
||||
|
||||
assert trade.has_open_orders
|
||||
# Process - shouldn't do anything
|
||||
freqtrade.process()
|
||||
# Doesn't try to exit, as we're not in a position yet
|
||||
assert freqtrade.strategy.custom_exit.call_count == 0
|
||||
|
||||
# Adjust with new price, cancel initial entry order and place new one
|
||||
freqtrade.strategy.adjust_entry_price = MagicMock(return_value=1.99)
|
||||
freqtrade.strategy.ft_check_timed_out = MagicMock(return_value=False)
|
||||
freqtrade.process()
|
||||
trade = Trade.get_trades().first()
|
||||
freqtrade.strategy.ft_check_timed_out = MagicMock(return_value=False)
|
||||
|
||||
assert len(trade.orders) == 2
|
||||
assert len(trade.open_orders) == 1
|
||||
|
||||
# Adjust with new amount, should cancel and replace existing order
|
||||
freqtrade.strategy.adjust_trade_position = MagicMock(
|
||||
return_value=21
|
||||
) # -(trade.stake_amount * 0.5)
|
||||
freqtrade.process()
|
||||
trade = Trade.get_trades().first()
|
||||
|
||||
assert len(trade.orders) == 3
|
||||
assert len(trade.open_orders) == 1
|
||||
|
||||
# Fill entry order
|
||||
assert freqtrade.strategy.custom_exit.call_count == 0
|
||||
|
||||
mocker.patch(f"{EXMS}._dry_is_price_crossed", return_value=True)
|
||||
freqtrade.process()
|
||||
|
||||
trade = Trade.get_trades().first()
|
||||
assert trade.amount > 0
|
||||
|
||||
assert freqtrade.strategy.custom_exit.call_count == 1
|
||||
freqtrade.strategy.custom_exit.reset_mock()
|
||||
|
||||
# Should Create a new exit order
|
||||
freqtrade.exchange.amount_to_contract_precision = MagicMock(return_value=2)
|
||||
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-2)
|
||||
|
||||
mocker.patch(f"{EXMS}._dry_is_price_crossed", return_value=False)
|
||||
freqtrade.process()
|
||||
trade = Trade.get_trades().first()
|
||||
|
||||
assert trade.orders[-2].status == "closed"
|
||||
assert trade.orders[-1].status == "open"
|
||||
assert trade.orders[-1].side == trade.exit_side
|
||||
assert len(trade.orders) == 5
|
||||
assert len(trade.open_orders) == 1
|
||||
assert freqtrade.strategy.custom_exit.call_count == 1
|
||||
freqtrade.strategy.custom_exit.reset_mock()
|
||||
|
||||
# Adjust with new exit amount, should cancel and replace existing exit order
|
||||
freqtrade.exchange.amount_to_contract_precision = MagicMock(return_value=3)
|
||||
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-3)
|
||||
freqtrade.process()
|
||||
trade = Trade.get_trades().first()
|
||||
# Even with open order, trying to exit...
|
||||
assert freqtrade.strategy.custom_exit.call_count == 1
|
||||
freqtrade.strategy.custom_exit.reset_mock()
|
||||
|
||||
assert trade.orders[-2].status == "canceled"
|
||||
assert len(trade.orders) == 6
|
||||
assert len(trade.open_orders) == 1
|
||||
|
||||
# Adjust with new exit price, should cancel and replace existing exit order
|
||||
freqtrade.strategy.custom_exit_price = MagicMock(return_value=1.95)
|
||||
freqtrade.process()
|
||||
# Even with open order, trying to exit...
|
||||
assert freqtrade.strategy.custom_exit.call_count == 1
|
||||
freqtrade.strategy.custom_exit.reset_mock()
|
||||
|
||||
trade = Trade.get_trades().first()
|
||||
|
||||
assert trade.orders[-2].status == "canceled"
|
||||
assert len(trade.orders) == 7
|
||||
assert len(trade.open_orders) == 1
|
||||
similar_msg = r"A similar open order was found for.*"
|
||||
|
||||
assert not log_has_re(similar_msg, caplog)
|
||||
|
||||
# Adjust with same params, should keep existing order as price and amount are similar
|
||||
freqtrade.strategy.custom_exit_price = MagicMock(return_value=1.95)
|
||||
freqtrade.process()
|
||||
trade = Trade.get_trades().first()
|
||||
assert log_has_re(similar_msg, caplog)
|
||||
|
||||
assert len(trade.orders) == 7
|
||||
assert len(trade.open_orders) == 1
|
||||
|
||||
@@ -45,6 +45,7 @@ class BTContainer(NamedTuple):
|
||||
leverage: float = 1.0
|
||||
timeout: int | None = None
|
||||
adjust_entry_price: float | None = None
|
||||
adjust_trade_position: list[float] | None = None
|
||||
|
||||
|
||||
def _get_frame_time_from_offset(offset):
|
||||
|
||||
@@ -1185,6 +1185,39 @@ tc56 = BTContainer(
|
||||
)
|
||||
|
||||
|
||||
# Test 57: Custom-entry-price for position adjustment which won't fill
|
||||
# Causing the negative adjustment to cancel the unfilled order and exit partially
|
||||
tc57 = BTContainer(
|
||||
data=[
|
||||
# D O H L C V EL XL ES Xs BT
|
||||
[0, 5000, 5050, 4950, 5000, 6172, 1, 0, 0, 0],
|
||||
[1, 4598, 5200, 4498, 5000, 6172, 0, 0, 0, 0],
|
||||
[2, 4900, 5250, 4900, 5100, 6172, 0, 0, 0, 0], # Enhance position, but won't fill
|
||||
[3, 5100, 5100, 4650, 4750, 6172, 0, 0, 0, 0],
|
||||
[4, 4750, 4950, 4650, 4750, 6172, 0, 0, 0, 0],
|
||||
[5, 4750, 4950, 4650, 4750, 6172, 0, 1, 0, 0],
|
||||
[6, 4750, 4950, 4650, 4750, 6172, 0, 0, 0, 0],
|
||||
],
|
||||
stop_loss=-0.2,
|
||||
roi={"0": 0.50},
|
||||
profit_perc=0.033,
|
||||
use_exit_signal=True,
|
||||
timeout=1000,
|
||||
custom_entry_price=4600,
|
||||
adjust_trade_position=[
|
||||
None,
|
||||
0.001,
|
||||
None,
|
||||
-0.0001, # Cancels the above unfilled order and exits partially
|
||||
None,
|
||||
None,
|
||||
],
|
||||
trades=[
|
||||
BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=1, close_tick=6, is_short=False),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
TESTS = [
|
||||
tc0,
|
||||
tc1,
|
||||
@@ -1243,6 +1276,7 @@ TESTS = [
|
||||
tc54,
|
||||
tc55,
|
||||
tc56,
|
||||
tc57,
|
||||
]
|
||||
|
||||
|
||||
@@ -1289,7 +1323,13 @@ def test_backtest_results(default_conf, mocker, caplog, data: BTContainer) -> No
|
||||
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.adjust_entry_price = MagicMock(return_value=data.adjust_entry_price)
|
||||
if data.adjust_trade_position:
|
||||
backtesting.strategy.position_adjustment_enable = True
|
||||
backtesting.strategy.adjust_trade_position = MagicMock(
|
||||
side_effect=data.adjust_trade_position
|
||||
)
|
||||
if data.adjust_entry_price:
|
||||
backtesting.strategy.adjust_entry_price = MagicMock(return_value=data.adjust_entry_price)
|
||||
|
||||
backtesting.strategy.use_custom_stoploss = data.use_custom_stoploss
|
||||
backtesting.strategy.leverage = lambda **kwargs: data.leverage
|
||||
@@ -1317,6 +1357,6 @@ def test_backtest_results(default_conf, mocker, caplog, data: BTContainer) -> No
|
||||
assert res.close_date == _get_frame_time_from_offset(trade.close_tick)
|
||||
assert res.is_short == trade.is_short
|
||||
assert len(LocalTrade.bt_trades) == len(data.trades)
|
||||
assert len(LocalTrade.bt_trades_open) == 0
|
||||
assert len(LocalTrade.bt_trades_open) == 0, "Left open trade"
|
||||
backtesting.cleanup()
|
||||
del backtesting
|
||||
|
||||
@@ -60,6 +60,7 @@ def test_trade_custom_data(fee, use_db):
|
||||
def test_trade_custom_data_strategy_compat(mocker, default_conf_usdt, fee):
|
||||
mocker.patch(f"{EXMS}.get_rate", return_value=0.50)
|
||||
mocker.patch("freqtrade.freqtradebot.FreqtradeBot.get_real_amount", return_value=None)
|
||||
mocker.patch("freqtrade.freqtradebot.FreqtradeBot.handle_cancel_exit", return_value=True)
|
||||
default_conf_usdt["minimal_roi"] = {"0": 100}
|
||||
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
@@ -85,8 +86,9 @@ def test_trade_custom_data_strategy_compat(mocker, default_conf_usdt, fee):
|
||||
trade_after = Trade.get_trades_proxy(pair="ADA/USDT")[0]
|
||||
assert trade_after.get_custom_data("test_str") == "test_value"
|
||||
assert trade_after.get_custom_data("test_int") == 1
|
||||
# 2 open pairs eligible for exit
|
||||
assert ff_spy.call_count == 2
|
||||
# 2 trades filled entry, with open exit order
|
||||
# 1 trade with filled entry order
|
||||
assert ff_spy.call_count == 3
|
||||
|
||||
assert trade_after.exit_reason == "test_value_1"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user