Include LIVE close-fee estimate in target PnL; rest after manual and emergency close.

Align docs so LIVE exit matches SIM fee buffer; manual full close now enters rest_seconds like auto/emergency.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-26 22:58:10 +08:00
parent 16efa44ffb
commit 9d3b36826c
7 changed files with 51 additions and 7 deletions
+5
View File
@@ -133,9 +133,14 @@ async def sim_close_group(_user: Annotated[str, Depends(require_user)]) -> dict:
status_code=403,
detail="未开启「显示手动开仓」;请在策略设置中开启后再用",
)
from ..strategy import get_engine
r = get_executor().close_group(reason="manual")
if not r.ok and not r.liquidity_wait:
raise HTTPException(status_code=400, detail=r.detail)
if r.ok:
# 与自动/紧急全平一致:成功全平后进入组间休息
get_engine().enter_rest_after_close()
return {
"ok": r.ok,
"liquidity_wait": r.liquidity_wait,
+2 -2
View File
@@ -72,7 +72,7 @@ def enrich_live_unrealized(
logger.warning("live unrealized exchange overlay failed: %s", e)
option_upl = float(base.get("option_upl") or 0.0) # 期权净盈亏(本地)
# 保守预估平仓费:按现有名义×费率×2 腿,避免「刚达标、扣费后不够」
# LIVE 达标口径:必须扣预估平仓手续费(与 SIM / 策略说明一致)
try:
from ..config import get_settings
@@ -89,7 +89,7 @@ def enrich_live_unrealized(
+ abs(float(base.get("option_qty_eth") or 0.0))
)
est_close = max(0.0, notional_est * fr * 2.0)
# 资金费 signed:付出为负,直接加总
# 资金费 signed:付出为负;再减估平仓费
net_pnl = perp_upl + option_upl - fees_paid + funding - est_close
out = dict(base)
+6 -2
View File
@@ -183,7 +183,7 @@ class StrategyEngine:
detail = r.detail
close_data = r.data
if r.ok:
self._after_close()
self.enter_rest_after_close()
elif st in ("open", "option_closed_perp_pending"):
# A:双腿(或续平永续)
r = self.matcher.close_group(reason="emergency", bypass_liquidity=True)
@@ -201,7 +201,7 @@ class StrategyEngine:
detail = r.detail
close_data = r.data
if r.ok:
self._after_close()
self.enter_rest_after_close()
residuals = self.matcher.settle_all_residuals_now()
return {
@@ -214,6 +214,10 @@ class StrategyEngine:
"state": self.state(),
}
def enter_rest_after_close(self) -> None:
"""全平成功后进入组间休息(自动 / 手动 / 紧急共用)。"""
self._after_close()
def _after_close(self) -> None:
s = get_settings()
row = self.db.fetchone("SELECT * FROM strategy_state WHERE id=1")
+22
View File
@@ -0,0 +1,22 @@
from __future__ import annotations
import time
from app.models.db import Database, set_db
from app.strategy.engine import StrategyEngine
def test_enter_rest_after_close_sets_resting(tmp_path) -> None:
db = Database(tmp_path / "rest.db")
set_db(db)
db.set_setting("rest_seconds", "120")
eng = StrategyEngine()
before = int(time.time() * 1000)
eng.enter_rest_after_close()
row = db.fetchone("SELECT * FROM strategy_state WHERE id=1")
assert row is not None
assert row["phase"] == "resting"
assert int(row["rounds_done"] or 0) >= 1
until = int(row["rest_until_ms"] or 0)
assert until >= before + 100_000
db.close()