Merge pull request #12617 from freqtrade/feat/exit_price

Add price parameter to force-exit API
This commit is contained in:
Matthias
2025-12-14 19:34:25 +01:00
committed by GitHub
6 changed files with 79 additions and 25 deletions
+10 -5
View File
@@ -2054,6 +2054,7 @@ class FreqtradeBot(LoggingMixin):
exit_tag: str | None = None, exit_tag: str | None = None,
ordertype: str | None = None, ordertype: str | None = None,
sub_trade_amt: float | None = None, sub_trade_amt: float | None = None,
skip_custom_exit_price: bool = False,
) -> bool: ) -> bool:
""" """
Executes a trade exit for the given trade and limit Executes a trade exit for the given trade and limit
@@ -2080,9 +2081,18 @@ class FreqtradeBot(LoggingMixin):
): ):
exit_type = "stoploss" exit_type = "stoploss"
order_type = (
(ordertype or self.strategy.order_types[exit_type])
if exit_check.exit_type != ExitType.EMERGENCY_EXIT
else self.strategy.order_types.get("emergency_exit", "market")
)
# set custom_exit_price if available # set custom_exit_price if available
proposed_limit_rate = limit proposed_limit_rate = limit
custom_exit_price = limit
current_profit = trade.calc_profit_ratio(limit) current_profit = trade.calc_profit_ratio(limit)
if order_type == "limit" and not skip_custom_exit_price:
custom_exit_price = strategy_safe_wrapper( custom_exit_price = strategy_safe_wrapper(
self.strategy.custom_exit_price, default_retval=proposed_limit_rate self.strategy.custom_exit_price, default_retval=proposed_limit_rate
)( )(
@@ -2099,11 +2109,6 @@ class FreqtradeBot(LoggingMixin):
# First cancelling stoploss on exchange ... # First cancelling stoploss on exchange ...
trade = self.cancel_stoploss_on_exchange(trade, allow_nonblocking=True) trade = self.cancel_stoploss_on_exchange(trade, allow_nonblocking=True)
order_type = ordertype or self.strategy.order_types[exit_type]
if exit_check.exit_type == ExitType.EMERGENCY_EXIT:
# Emergency exits (default to market!)
order_type = self.strategy.order_types.get("emergency_exit", "market")
amount = self._safe_exit_amount(trade, trade.pair, sub_trade_amt or trade.amount) amount = self._safe_exit_amount(trade, trade.pair, sub_trade_amt or trade.amount)
time_in_force = self.strategy.order_time_in_force["exit"] time_in_force = self.strategy.order_time_in_force["exit"]
+1
View File
@@ -426,6 +426,7 @@ class ForceExitPayload(BaseModel):
tradeid: str | int tradeid: str | int
ordertype: OrderTypeValues | None = None ordertype: OrderTypeValues | None = None
amount: float | None = None amount: float | None = None
price: float | None = None
class BlacklistPayload(BaseModel): class BlacklistPayload(BaseModel):
+5 -2
View File
@@ -92,7 +92,8 @@ logger = logging.getLogger(__name__)
# 2.42: Add /pair_history endpoint with live data # 2.42: Add /pair_history endpoint with live data
# 2.43: Add /profit_all endpoint # 2.43: Add /profit_all endpoint
# 2.44: Add candle_types parameter to download-data endpoint # 2.44: Add candle_types parameter to download-data endpoint
API_VERSION = 2.44 # 2.45: Add price to forceexit endpoint
API_VERSION = 2.45
# Public API, requires no auth. # Public API, requires no auth.
router_public = APIRouter() router_public = APIRouter()
@@ -325,7 +326,9 @@ def force_entry(payload: ForceEnterPayload, rpc: RPC = Depends(get_rpc)):
@router.post("/forcesell", response_model=ResultMsg, tags=["trading"]) @router.post("/forcesell", response_model=ResultMsg, tags=["trading"])
def forceexit(payload: ForceExitPayload, rpc: RPC = Depends(get_rpc)): def forceexit(payload: ForceExitPayload, rpc: RPC = Depends(get_rpc)):
ordertype = payload.ordertype.value if payload.ordertype else None ordertype = payload.ordertype.value if payload.ordertype else None
return rpc._rpc_force_exit(str(payload.tradeid), ordertype, amount=payload.amount) return rpc._rpc_force_exit(
str(payload.tradeid), ordertype, amount=payload.amount, price=payload.price
)
@router.get("/blacklist", response_model=BlacklistResponse, tags=["info", "pairlist"]) @router.get("/blacklist", response_model=BlacklistResponse, tags=["info", "pairlist"])
+25 -6
View File
@@ -940,7 +940,11 @@ class RPC:
return {"status": "Reloaded from orders from exchange"} return {"status": "Reloaded from orders from exchange"}
def __exec_force_exit( def __exec_force_exit(
self, trade: Trade, ordertype: str | None, amount: float | None = None self,
trade: Trade,
ordertype: str | None,
amount: float | None = None,
price: float | None = None,
) -> bool: ) -> bool:
# Check if there is there are open orders # Check if there is there are open orders
trade_entry_cancelation_registry = [] trade_entry_cancelation_registry = []
@@ -964,9 +968,14 @@ class RPC:
# Order cancellation failed, so we can't exit. # Order cancellation failed, so we can't exit.
return False return False
# Get current rate and execute sell # Get current rate and execute sell
current_rate = self._freqtrade.exchange.get_rate(
current_rate = (
self._freqtrade.exchange.get_rate(
trade.pair, side="exit", is_short=trade.is_short, refresh=True trade.pair, side="exit", is_short=trade.is_short, refresh=True
) )
if ordertype == "market" or price is None
else price
)
exit_check = ExitCheckTuple(exit_type=ExitType.FORCE_EXIT) exit_check = ExitCheckTuple(exit_type=ExitType.FORCE_EXIT)
order_type = ordertype or self._freqtrade.strategy.order_types.get( order_type = ordertype or self._freqtrade.strategy.order_types.get(
"force_exit", self._freqtrade.strategy.order_types["exit"] "force_exit", self._freqtrade.strategy.order_types["exit"]
@@ -983,18 +992,28 @@ class RPC:
sub_amount = amount sub_amount = amount
self._freqtrade.execute_trade_exit( self._freqtrade.execute_trade_exit(
trade, current_rate, exit_check, ordertype=order_type, sub_trade_amt=sub_amount trade,
current_rate,
exit_check,
ordertype=order_type,
sub_trade_amt=sub_amount,
skip_custom_exit_price=price is not None and ordertype == "limit",
) )
return True return True
return False return False
def _rpc_force_exit( def _rpc_force_exit(
self, trade_id: str, ordertype: str | None = None, *, amount: float | None = None self,
trade_id: str,
ordertype: str | None = None,
*,
amount: float | None = None,
price: float | None = None,
) -> dict[str, str]: ) -> dict[str, str]:
""" """
Handler for forceexit <id>. Handler for forceexit <id>.
Sells the given trade at current price exits the given trade. Uses current price if price is None.
""" """
if self._freqtrade.state == State.STOPPED: if self._freqtrade.state == State.STOPPED:
@@ -1024,7 +1043,7 @@ class RPC:
logger.warning("force_exit: Invalid argument received") logger.warning("force_exit: Invalid argument received")
raise RPCException("invalid argument") raise RPCException("invalid argument")
result = self.__exec_force_exit(trade, ordertype, amount) result = self.__exec_force_exit(trade, ordertype, amount, price)
Trade.commit() Trade.commit()
self._freqtrade.wallets.update() self._freqtrade.wallets.update()
if not result: if not result:
+1 -1
View File
@@ -3092,7 +3092,7 @@ def test_execute_trade_exit_custom_exit_price(
"exit_reason": "foo", "exit_reason": "foo",
"open_date": ANY, "open_date": ANY,
"close_date": ANY, "close_date": ANY,
"close_rate": ANY, "close_rate": 2.25, # the custom exit price
"sub_trade": False, "sub_trade": False,
"cumulative_profit": 0.0, "cumulative_profit": 0.0,
"stake_amount": pytest.approx(60), "stake_amount": pytest.approx(60),
+26
View File
@@ -1852,9 +1852,35 @@ def test_api_forceexit(botclient, mocker, ticker, fee, markets):
Trade.rollback() Trade.rollback()
trade = Trade.get_trades([Trade.id == 5]).first() trade = Trade.get_trades([Trade.id == 5]).first()
last_order = trade.orders[-1]
assert last_order.side == "sell"
assert last_order.status == "closed"
assert last_order.order_type == "market"
assert last_order.amount == 23
assert pytest.approx(trade.amount) == 100 assert pytest.approx(trade.amount) == 100
assert trade.is_open is True assert trade.is_open is True
# Test with explicit price
rc = client_post(
client,
f"{BASE_URI}/forceexit",
data={"tradeid": "5", "ordertype": "limit", "amount": 25, "price": 0.12345},
)
assert_response(rc)
assert rc.json() == {"result": "Created exit order for trade 5."}
Trade.rollback()
trade = Trade.get_trades([Trade.id == 5]).first()
last_order = trade.orders[-1]
assert last_order.status == "closed"
assert last_order.order_type == "limit"
assert pytest.approx(last_order.safe_price) == 0.12345
assert pytest.approx(last_order.amount) == 25
assert pytest.approx(trade.amount) == 75
assert trade.is_open is True
rc = client_post(client, f"{BASE_URI}/forceexit", data={"tradeid": "5"}) rc = client_post(client, f"{BASE_URI}/forceexit", data={"tradeid": "5"})
assert_response(rc) assert_response(rc)
assert rc.json() == {"result": "Created exit order for trade 5."} assert rc.json() == {"result": "Created exit order for trade 5."}