Merge pull request #11412 from freqtrade/feat/adjust_order_price

Add adjust_order_price callback
This commit is contained in:
Matthias
2025-02-24 07:02:27 +01:00
committed by GitHub
12 changed files with 356 additions and 77 deletions

View File

@@ -436,6 +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_exit_price = MagicMock(side_effect=ValueError)
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=None)
freqtrade.process()
trade = Trade.get_trades().first()
@@ -445,6 +446,8 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
assert pytest.approx(trade.stake_amount) == 60
assert trade.orders[-1].price == 1.95
assert pytest.approx(trade.orders[-1].cost) == 120 * leverage
assert freqtrade.strategy.adjust_entry_price.call_count == 1
assert freqtrade.strategy.adjust_exit_price.call_count == 0
# Fill DCA order
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=None)
@@ -469,6 +472,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
mocker.patch(f"{EXMS}._dry_is_price_crossed", return_value=False)
freqtrade.strategy.custom_exit = MagicMock(return_value="Exit now")
freqtrade.strategy.adjust_entry_price = MagicMock(return_value=2.02)
freqtrade.strategy.adjust_exit_price = MagicMock(side_effect=ValueError)
freqtrade.process()
trade = Trade.get_trades().first()
assert len(trade.orders) == 5
@@ -478,8 +482,9 @@ 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
assert freqtrade.strategy.adjust_exit_price.call_count == 0
# Process again, should not adjust entry price
# Process again, should not adjust price
freqtrade.process()
trade = Trade.get_trades().first()
@@ -490,6 +495,21 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
assert trade.orders[-1].price == 2.02
# Adjust entry price cannot be called - this is an exit order
assert freqtrade.strategy.adjust_entry_price.call_count == 0
assert freqtrade.strategy.adjust_exit_price.call_count == 1
freqtrade.strategy.adjust_exit_price = MagicMock(return_value=2.03)
# Process again, should adjust exit price
freqtrade.process()
trade = Trade.get_trades().first()
assert trade.orders[-2].status == "canceled"
assert len(trade.orders) == 6
assert trade.orders[-1].side == trade.exit_side
assert trade.orders[-1].status == "open"
assert trade.orders[-1].price == 2.03
assert freqtrade.strategy.adjust_entry_price.call_count == 0
assert freqtrade.strategy.adjust_exit_price.call_count == 1
@pytest.mark.parametrize("leverage", [1, 2])

View File

@@ -45,6 +45,7 @@ class BTContainer(NamedTuple):
leverage: float = 1.0
timeout: int | None = None
adjust_entry_price: float | None = None
adjust_exit_price: float | None = None
adjust_trade_position: list[float] | None = None

View File

@@ -1217,6 +1217,46 @@ tc57 = BTContainer(
],
)
# Test 58: Custom-exit-price short - below all candles
tc58 = BTContainer(
data=[
# D O H L C V EL XL ES Xs BT
[0, 5000, 5050, 4950, 5000, 6172, 0, 0, 1, 0],
[1, 5000, 5200, 4951, 5000, 6172, 0, 0, 0, 0], # enter trade (signal on last candle)
[2, 4900, 5250, 4900, 5100, 6172, 0, 0, 0, 1], # Exit - delayed
[3, 5100, 5100, 4650, 4750, 6172, 0, 0, 0, 0], #
[4, 4750, 5100, 4350, 4750, 6172, 0, 0, 0, 0],
],
stop_loss=-0.10,
roi={"0": 1.00},
profit_perc=-0.01,
use_exit_signal=True,
timeout=1000,
custom_exit_price=4300,
adjust_exit_price=5050,
trades=[BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=1, close_tick=4, is_short=True)],
)
# Test 59: Custom-exit-price above all candles - readjust order
tc59 = BTContainer(
data=[
# D O H L C V EL XL ES Xs BT
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
[1, 5000, 5500, 4951, 5000, 6172, 0, 0],
[2, 4900, 5250, 4500, 5100, 6172, 0, 1], # exit
[3, 5100, 5100, 4650, 4750, 6172, 0, 0], # order readjust
[4, 4750, 4950, 4350, 4750, 6172, 0, 0],
],
stop_loss=-0.2,
roi={"0": 0.10},
profit_perc=-0.02,
use_exit_signal=True,
timeout=1000,
custom_exit_price=5300,
adjust_exit_price=4900,
trades=[BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=1, close_tick=4, is_short=False)],
)
TESTS = [
tc0,
@@ -1277,6 +1317,8 @@ TESTS = [
tc55,
tc56,
tc57,
tc58,
tc59,
]
@@ -1330,6 +1372,8 @@ def test_backtest_results(default_conf, mocker, caplog, data: BTContainer) -> No
)
if data.adjust_entry_price:
backtesting.strategy.adjust_entry_price = MagicMock(return_value=data.adjust_entry_price)
if data.adjust_exit_price:
backtesting.strategy.adjust_exit_price = MagicMock(return_value=data.adjust_exit_price)
backtesting.strategy.use_custom_stoploss = data.use_custom_stoploss
backtesting.strategy.leverage = lambda **kwargs: data.leverage

View File

@@ -21,10 +21,12 @@ class TestStrategyNoImplementSell(TestStrategyNoImplements):
return super().populate_entry_trend(dataframe, metadata)
class TestStrategyImplementCustomSell(TestStrategyNoImplementSell):
class TestStrategyImplementEmptyWorking(TestStrategyNoImplementSell):
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
return super().populate_exit_trend(dataframe, metadata)
class TestStrategyImplementCustomSell(TestStrategyImplementEmptyWorking):
def custom_sell(
self,
pair: str,
@@ -55,3 +57,34 @@ class TestStrategyImplementSellTimeout(TestStrategyNoImplementSell):
self, pair: str, trade, order: Order, current_time: datetime, **kwargs
) -> bool:
return False
class TestStrategyAdjustOrderPrice(TestStrategyImplementEmptyWorking):
def adjust_entry_price(
self,
trade,
order,
pair,
current_time,
proposed_rate,
current_order_rate,
entry_tag,
side,
**kwargs,
):
return proposed_rate
def adjust_order_price(
self,
trade,
order,
pair,
current_time,
proposed_rate,
current_order_rate,
entry_tag,
side,
is_entry,
**kwargs,
):
return proposed_rate

View File

@@ -460,6 +460,10 @@ def test_missing_implements(default_conf, caplog):
):
StrategyResolver.load_strategy(default_conf)
default_conf["strategy"] = "TestStrategyAdjustOrderPrice"
with pytest.raises(OperationalException, match=r"If you implement `adjust_order_price`.*"):
StrategyResolver.load_strategy(default_conf)
def test_call_deprecated_function(default_conf):
default_location = Path(__file__).parent / "strats/broken_strats/"