Skip to content

Commit e52b5ae

Browse files
authored
Merge pull request freqtrade#12617 from freqtrade/feat/exit_price
Add price parameter to force-exit API
2 parents 8af0631 + a16d2a1 commit e52b5ae

File tree

6 files changed

+79
-25
lines changed

6 files changed

+79
-25
lines changed

freqtrade/freqtradebot.py

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2054,6 +2054,7 @@ def execute_trade_exit(
20542054
exit_tag: str | None = None,
20552055
ordertype: str | None = None,
20562056
sub_trade_amt: float | None = None,
2057+
skip_custom_exit_price: bool = False,
20572058
) -> bool:
20582059
"""
20592060
Executes a trade exit for the given trade and limit
@@ -2080,30 +2081,34 @@ def execute_trade_exit(
20802081
):
20812082
exit_type = "stoploss"
20822083

2084+
order_type = (
2085+
(ordertype or self.strategy.order_types[exit_type])
2086+
if exit_check.exit_type != ExitType.EMERGENCY_EXIT
2087+
else self.strategy.order_types.get("emergency_exit", "market")
2088+
)
2089+
20832090
# set custom_exit_price if available
20842091
proposed_limit_rate = limit
2092+
custom_exit_price = limit
2093+
20852094
current_profit = trade.calc_profit_ratio(limit)
2086-
custom_exit_price = strategy_safe_wrapper(
2087-
self.strategy.custom_exit_price, default_retval=proposed_limit_rate
2088-
)(
2089-
pair=trade.pair,
2090-
trade=trade,
2091-
current_time=datetime.now(UTC),
2092-
proposed_rate=proposed_limit_rate,
2093-
current_profit=current_profit,
2094-
exit_tag=exit_reason,
2095-
)
2095+
if order_type == "limit" and not skip_custom_exit_price:
2096+
custom_exit_price = strategy_safe_wrapper(
2097+
self.strategy.custom_exit_price, default_retval=proposed_limit_rate
2098+
)(
2099+
pair=trade.pair,
2100+
trade=trade,
2101+
current_time=datetime.now(UTC),
2102+
proposed_rate=proposed_limit_rate,
2103+
current_profit=current_profit,
2104+
exit_tag=exit_reason,
2105+
)
20962106

20972107
limit = self.get_valid_price(custom_exit_price, proposed_limit_rate)
20982108

20992109
# First cancelling stoploss on exchange ...
21002110
trade = self.cancel_stoploss_on_exchange(trade, allow_nonblocking=True)
21012111

2102-
order_type = ordertype or self.strategy.order_types[exit_type]
2103-
if exit_check.exit_type == ExitType.EMERGENCY_EXIT:
2104-
# Emergency exits (default to market!)
2105-
order_type = self.strategy.order_types.get("emergency_exit", "market")
2106-
21072112
amount = self._safe_exit_amount(trade, trade.pair, sub_trade_amt or trade.amount)
21082113
time_in_force = self.strategy.order_time_in_force["exit"]
21092114

freqtrade/rpc/api_server/api_schemas.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,7 @@ class ForceExitPayload(BaseModel):
426426
tradeid: str | int
427427
ordertype: OrderTypeValues | None = None
428428
amount: float | None = None
429+
price: float | None = None
429430

430431

431432
class BlacklistPayload(BaseModel):

freqtrade/rpc/api_server/api_v1.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@
9292
# 2.42: Add /pair_history endpoint with live data
9393
# 2.43: Add /profit_all endpoint
9494
# 2.44: Add candle_types parameter to download-data endpoint
95-
API_VERSION = 2.44
95+
# 2.45: Add price to forceexit endpoint
96+
API_VERSION = 2.45
9697

9798
# Public API, requires no auth.
9899
router_public = APIRouter()
@@ -325,7 +326,9 @@ def force_entry(payload: ForceEnterPayload, rpc: RPC = Depends(get_rpc)):
325326
@router.post("/forcesell", response_model=ResultMsg, tags=["trading"])
326327
def forceexit(payload: ForceExitPayload, rpc: RPC = Depends(get_rpc)):
327328
ordertype = payload.ordertype.value if payload.ordertype else None
328-
return rpc._rpc_force_exit(str(payload.tradeid), ordertype, amount=payload.amount)
329+
return rpc._rpc_force_exit(
330+
str(payload.tradeid), ordertype, amount=payload.amount, price=payload.price
331+
)
329332

330333

331334
@router.get("/blacklist", response_model=BlacklistResponse, tags=["info", "pairlist"])

freqtrade/rpc/rpc.py

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -940,7 +940,11 @@ def _rpc_reload_trade_from_exchange(self, trade_id: int) -> dict[str, str]:
940940
return {"status": "Reloaded from orders from exchange"}
941941

942942
def __exec_force_exit(
943-
self, trade: Trade, ordertype: str | None, amount: float | None = None
943+
self,
944+
trade: Trade,
945+
ordertype: str | None,
946+
amount: float | None = None,
947+
price: float | None = None,
944948
) -> bool:
945949
# Check if there is there are open orders
946950
trade_entry_cancelation_registry = []
@@ -964,8 +968,13 @@ def __exec_force_exit(
964968
# Order cancellation failed, so we can't exit.
965969
return False
966970
# Get current rate and execute sell
967-
current_rate = self._freqtrade.exchange.get_rate(
968-
trade.pair, side="exit", is_short=trade.is_short, refresh=True
971+
972+
current_rate = (
973+
self._freqtrade.exchange.get_rate(
974+
trade.pair, side="exit", is_short=trade.is_short, refresh=True
975+
)
976+
if ordertype == "market" or price is None
977+
else price
969978
)
970979
exit_check = ExitCheckTuple(exit_type=ExitType.FORCE_EXIT)
971980
order_type = ordertype or self._freqtrade.strategy.order_types.get(
@@ -983,18 +992,28 @@ def __exec_force_exit(
983992
sub_amount = amount
984993

985994
self._freqtrade.execute_trade_exit(
986-
trade, current_rate, exit_check, ordertype=order_type, sub_trade_amt=sub_amount
995+
trade,
996+
current_rate,
997+
exit_check,
998+
ordertype=order_type,
999+
sub_trade_amt=sub_amount,
1000+
skip_custom_exit_price=price is not None and ordertype == "limit",
9871001
)
9881002

9891003
return True
9901004
return False
9911005

9921006
def _rpc_force_exit(
993-
self, trade_id: str, ordertype: str | None = None, *, amount: float | None = None
1007+
self,
1008+
trade_id: str,
1009+
ordertype: str | None = None,
1010+
*,
1011+
amount: float | None = None,
1012+
price: float | None = None,
9941013
) -> dict[str, str]:
9951014
"""
9961015
Handler for forceexit <id>.
997-
Sells the given trade at current price
1016+
exits the given trade. Uses current price if price is None.
9981017
"""
9991018

10001019
if self._freqtrade.state == State.STOPPED:
@@ -1024,7 +1043,7 @@ def _rpc_force_exit(
10241043
logger.warning("force_exit: Invalid argument received")
10251044
raise RPCException("invalid argument")
10261045

1027-
result = self.__exec_force_exit(trade, ordertype, amount)
1046+
result = self.__exec_force_exit(trade, ordertype, amount, price)
10281047
Trade.commit()
10291048
self._freqtrade.wallets.update()
10301049
if not result:

tests/freqtradebot/test_freqtradebot.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3092,7 +3092,7 @@ def test_execute_trade_exit_custom_exit_price(
30923092
"exit_reason": "foo",
30933093
"open_date": ANY,
30943094
"close_date": ANY,
3095-
"close_rate": ANY,
3095+
"close_rate": 2.25, # the custom exit price
30963096
"sub_trade": False,
30973097
"cumulative_profit": 0.0,
30983098
"stake_amount": pytest.approx(60),

tests/rpc/test_rpc_apiserver.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1852,9 +1852,35 @@ def test_api_forceexit(botclient, mocker, ticker, fee, markets):
18521852
Trade.rollback()
18531853

18541854
trade = Trade.get_trades([Trade.id == 5]).first()
1855+
last_order = trade.orders[-1]
1856+
1857+
assert last_order.side == "sell"
1858+
assert last_order.status == "closed"
1859+
assert last_order.order_type == "market"
1860+
assert last_order.amount == 23
18551861
assert pytest.approx(trade.amount) == 100
18561862
assert trade.is_open is True
18571863

1864+
# Test with explicit price
1865+
rc = client_post(
1866+
client,
1867+
f"{BASE_URI}/forceexit",
1868+
data={"tradeid": "5", "ordertype": "limit", "amount": 25, "price": 0.12345},
1869+
)
1870+
assert_response(rc)
1871+
assert rc.json() == {"result": "Created exit order for trade 5."}
1872+
Trade.rollback()
1873+
1874+
trade = Trade.get_trades([Trade.id == 5]).first()
1875+
last_order = trade.orders[-1]
1876+
assert last_order.status == "closed"
1877+
assert last_order.order_type == "limit"
1878+
assert pytest.approx(last_order.safe_price) == 0.12345
1879+
assert pytest.approx(last_order.amount) == 25
1880+
1881+
assert pytest.approx(trade.amount) == 75
1882+
assert trade.is_open is True
1883+
18581884
rc = client_post(client, f"{BASE_URI}/forceexit", data={"tradeid": "5"})
18591885
assert_response(rc)
18601886
assert rc.json() == {"result": "Created exit order for trade 5."}

0 commit comments

Comments
 (0)