Merge pull request #12617 from freqtrade/feat/exit_price
Add price parameter to force-exit API
This commit is contained in:
+20
-15
@@ -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,30 +2081,34 @@ 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)
|
||||||
custom_exit_price = strategy_safe_wrapper(
|
if order_type == "limit" and not skip_custom_exit_price:
|
||||||
self.strategy.custom_exit_price, default_retval=proposed_limit_rate
|
custom_exit_price = strategy_safe_wrapper(
|
||||||
)(
|
self.strategy.custom_exit_price, default_retval=proposed_limit_rate
|
||||||
pair=trade.pair,
|
)(
|
||||||
trade=trade,
|
pair=trade.pair,
|
||||||
current_time=datetime.now(UTC),
|
trade=trade,
|
||||||
proposed_rate=proposed_limit_rate,
|
current_time=datetime.now(UTC),
|
||||||
current_profit=current_profit,
|
proposed_rate=proposed_limit_rate,
|
||||||
exit_tag=exit_reason,
|
current_profit=current_profit,
|
||||||
)
|
exit_tag=exit_reason,
|
||||||
|
)
|
||||||
|
|
||||||
limit = self.get_valid_price(custom_exit_price, proposed_limit_rate)
|
limit = self.get_valid_price(custom_exit_price, proposed_limit_rate)
|
||||||
|
|
||||||
# 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"]
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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"])
|
||||||
|
|||||||
+26
-7
@@ -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,8 +968,13 @@ 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(
|
|
||||||
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)
|
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(
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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."}
|
||||||
|
|||||||
Reference in New Issue
Block a user