mirror of
https://github.com/freqtrade/freqtrade.git
synced 2026-01-20 14:00:38 +00:00
Merge pull request #12617 from freqtrade/feat/exit_price
Add price parameter to force-exit API
This commit is contained in:
@@ -2054,6 +2054,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
exit_tag: str | None = None,
|
||||
ordertype: str | None = None,
|
||||
sub_trade_amt: float | None = None,
|
||||
skip_custom_exit_price: bool = False,
|
||||
) -> bool:
|
||||
"""
|
||||
Executes a trade exit for the given trade and limit
|
||||
@@ -2080,30 +2081,34 @@ class FreqtradeBot(LoggingMixin):
|
||||
):
|
||||
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
|
||||
proposed_limit_rate = limit
|
||||
custom_exit_price = limit
|
||||
|
||||
current_profit = trade.calc_profit_ratio(limit)
|
||||
custom_exit_price = strategy_safe_wrapper(
|
||||
self.strategy.custom_exit_price, default_retval=proposed_limit_rate
|
||||
)(
|
||||
pair=trade.pair,
|
||||
trade=trade,
|
||||
current_time=datetime.now(UTC),
|
||||
proposed_rate=proposed_limit_rate,
|
||||
current_profit=current_profit,
|
||||
exit_tag=exit_reason,
|
||||
)
|
||||
if order_type == "limit" and not skip_custom_exit_price:
|
||||
custom_exit_price = strategy_safe_wrapper(
|
||||
self.strategy.custom_exit_price, default_retval=proposed_limit_rate
|
||||
)(
|
||||
pair=trade.pair,
|
||||
trade=trade,
|
||||
current_time=datetime.now(UTC),
|
||||
proposed_rate=proposed_limit_rate,
|
||||
current_profit=current_profit,
|
||||
exit_tag=exit_reason,
|
||||
)
|
||||
|
||||
limit = self.get_valid_price(custom_exit_price, proposed_limit_rate)
|
||||
|
||||
# First cancelling stoploss on exchange ...
|
||||
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)
|
||||
time_in_force = self.strategy.order_time_in_force["exit"]
|
||||
|
||||
|
||||
@@ -426,6 +426,7 @@ class ForceExitPayload(BaseModel):
|
||||
tradeid: str | int
|
||||
ordertype: OrderTypeValues | None = None
|
||||
amount: float | None = None
|
||||
price: float | None = None
|
||||
|
||||
|
||||
class BlacklistPayload(BaseModel):
|
||||
|
||||
@@ -92,7 +92,8 @@ logger = logging.getLogger(__name__)
|
||||
# 2.42: Add /pair_history endpoint with live data
|
||||
# 2.43: Add /profit_all 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.
|
||||
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"])
|
||||
def forceexit(payload: ForceExitPayload, rpc: RPC = Depends(get_rpc)):
|
||||
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"])
|
||||
|
||||
@@ -940,7 +940,11 @@ class RPC:
|
||||
return {"status": "Reloaded from orders from exchange"}
|
||||
|
||||
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:
|
||||
# Check if there is there are open orders
|
||||
trade_entry_cancelation_registry = []
|
||||
@@ -964,8 +968,13 @@ class RPC:
|
||||
# Order cancellation failed, so we can't exit.
|
||||
return False
|
||||
# Get current rate and execute sell
|
||||
current_rate = self._freqtrade.exchange.get_rate(
|
||||
trade.pair, side="exit", is_short=trade.is_short, refresh=True
|
||||
|
||||
current_rate = (
|
||||
self._freqtrade.exchange.get_rate(
|
||||
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)
|
||||
order_type = ordertype or self._freqtrade.strategy.order_types.get(
|
||||
@@ -983,18 +992,28 @@ class RPC:
|
||||
sub_amount = amount
|
||||
|
||||
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 False
|
||||
|
||||
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]:
|
||||
"""
|
||||
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:
|
||||
@@ -1024,7 +1043,7 @@ class RPC:
|
||||
logger.warning("force_exit: Invalid argument received")
|
||||
raise RPCException("invalid argument")
|
||||
|
||||
result = self.__exec_force_exit(trade, ordertype, amount)
|
||||
result = self.__exec_force_exit(trade, ordertype, amount, price)
|
||||
Trade.commit()
|
||||
self._freqtrade.wallets.update()
|
||||
if not result:
|
||||
|
||||
@@ -3092,7 +3092,7 @@ def test_execute_trade_exit_custom_exit_price(
|
||||
"exit_reason": "foo",
|
||||
"open_date": ANY,
|
||||
"close_date": ANY,
|
||||
"close_rate": ANY,
|
||||
"close_rate": 2.25, # the custom exit price
|
||||
"sub_trade": False,
|
||||
"cumulative_profit": 0.0,
|
||||
"stake_amount": pytest.approx(60),
|
||||
|
||||
@@ -1852,9 +1852,35 @@ def test_api_forceexit(botclient, mocker, ticker, fee, markets):
|
||||
Trade.rollback()
|
||||
|
||||
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 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"})
|
||||
assert_response(rc)
|
||||
assert rc.json() == {"result": "Created exit order for trade 5."}
|
||||
|
||||
Reference in New Issue
Block a user