Fix target-exit mismatch: correct close reason and executable PnL gate.

Locked premium exits no longer label as fixed_usdt; mark/book optimism no longer triggers close into a realized loss.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-07 22:05:34 +08:00
parent a88d708ea3
commit bf3441537e
6 changed files with 74 additions and 31 deletions
+15 -7
View File
@@ -73,20 +73,28 @@ def enrich_live_unrealized(
option_upl = float(base.get("option_upl") or 0.0) # 期权净盈亏(本地)
option2_upl = float(base.get("option2_upl") or 0.0) # 期期 Put 腿
# 盯盘/达标:离场费 ≈ 入场费 → 合计扣 已付×2
est_close = float(fees_paid)
# 期期无永续:perp_upl 一般为 0;仍加 option2
net_pnl = perp_upl + option_upl + option2_upl - fees_paid * 2.0 + funding
# 盯盘/达标:离场费已在 base.net_pnl(盘口可成交价)计入;这里只叠加资金费。
# 勿用交易所永续标记 UPL 覆盖净利,否则会虚高触发达标、成交后变亏。
book_net = base.get("net_pnl")
if book_net is not None:
net_pnl = float(book_net) + funding
else:
fees_est = float(fees_paid) * 2.0
net_pnl = perp_upl + option_upl + option2_upl - fees_est + funding
out = dict(base)
out["perp_upl"] = perp_upl
out["perp_upl"] = float(base.get("perp_upl") or perp_upl)
out["perp_upl_exchange"] = perp_upl
out["option_upl"] = option_upl
out["option2_upl"] = option2_upl
out["fees_paid"] = fees_paid
out["funding_usdt"] = funding
out["est_close_fees"] = est_close
out["est_close_fees"] = float(base.get("est_close_fees") or fees_paid)
out["net_pnl"] = net_pnl
out["pnl_source"] = "live_exchange"
out["net_pnl_exchange"] = (
perp_upl + option_upl + option2_upl - float(fees_paid) * 2.0 + funding
)
out["pnl_source"] = "live_book_plus_funding"
return out
+17 -9
View File
@@ -2233,13 +2233,19 @@ class Matcher:
est_opt_close_fee = of.fee
opt_mark = bid
opt_bid_sz = float(oq.bid_sz) if oq.bid_sz is not None else None
# 浮盈亏:买一×数量 − 初始权利金(对齐可市价卖出
option_upl = bid * opt_qty - initial_premium
# 与 close_group 一致:用含费率调整的可卖出价(勿用裸买一虚高触发达标
if opt_entry > 0:
option_upl = (float(of.fill_px) - float(opt_entry)) * opt_qty
else:
option_upl = float(of.fill_px) * opt_qty - initial_premium
elif oq:
opt_mark = oq.bid or oq.mark_px
opt_bid_sz = float(oq.bid_sz) if oq.bid_sz is not None else None
if opt_mark is not None:
option_upl = float(opt_mark) * opt_qty - initial_premium
if opt_entry > 0:
option_upl = (float(opt_mark) - float(opt_entry)) * opt_qty
else:
option_upl = float(opt_mark) * opt_qty - initial_premium
book_close_fees = est_perp_close_fee + est_opt_close_fee
@@ -2266,13 +2272,15 @@ class Matcher:
expiry_ymd = str(g["expiry_ymd"]) if g and g["expiry_ymd"] else None
open_at_ms = int(g["open_at_ms"]) if g and g["open_at_ms"] else None
open_fees = abs(float(g["fees"] or 0)) if g else 0.0
# 净盈利:浮盈 入场手续费×2(离场费按入场估算);无入场费时退回盘口估平仓费
if open_fees > 1e-12:
est_close_fees = open_fees
net_pnl = perp_upl + option_upl - open_fees * 2.0
# 净盈利:浮盈 已付开仓费 − 预估平仓费(与 close_group 入账口径对齐)
if book_close_fees > 1e-12 or open_fees > 1e-12:
est_close_fees = (
book_close_fees if book_close_fees > 1e-12 else open_fees
)
net_pnl = perp_upl + option_upl - open_fees - est_close_fees
else:
est_close_fees = book_close_fees
net_pnl = perp_upl + option_upl - est_close_fees
est_close_fees = 0.0
net_pnl = perp_upl + option_upl
expiry_ms = None
if expiry_ymd and len(expiry_ymd) == 6:
try:
+11
View File
@@ -882,6 +882,17 @@ class StrategyEngine:
locked_exit_target=locked_exit,
)
pending_close = st["phase"] in ("liquidity_wait", "closing")
# 曾因达标进入流动性等待,但当前估价已跌破目标:取消挂起,避免硬平出亏损却仍记「达标」
if (
pending_close
and not expired.should_close
and not decision.should_close
):
self._set_state(
phase="open",
last_error="达标后流动性等待期间浮盈回落,已取消平仓挂起",
)
return
if expired.should_close or decision.should_close or pending_close:
is_oo = (
str(upl.get("hedge_mode") or "") == "option_option"
+8 -7
View File
@@ -110,16 +110,17 @@ def check_exits(
locked_exit_target: float | None = None,
) -> ExitDecision:
"""净盈利(预估全平后)≥ 所选模式目标则全平。持仓锁定目标优先。"""
# 模式始终按设置解析(权利金倍数 vs 固定),勿因锁定目标就改成 fixed_usdt
_resolved_target, mode = resolve_exit_target(
exit_mode=exit_mode,
net_profit_target=net_profit_target,
premium_exit_multiple=premium_exit_multiple,
initial_premium=initial_premium,
)
if locked_exit_target is not None and float(locked_exit_target) > 0:
target = float(locked_exit_target)
mode = EXIT_MODE_FIXED
else:
target, mode = resolve_exit_target(
exit_mode=exit_mode,
net_profit_target=net_profit_target,
premium_exit_multiple=premium_exit_multiple,
initial_premium=initial_premium,
)
target = float(_resolved_target)
if target > 0 and net_pnl + 1e-9 >= target:
reason = "premium_multiple" if mode == EXIT_MODE_PREMIUM else "fixed_usdt"
return ExitDecision(True, reason, target)
+15
View File
@@ -5,6 +5,21 @@ from __future__ import annotations
from app.strategy.exits import check_exits, resolve_exit_target
def test_locked_premium_mode_keeps_premium_reason() -> None:
"""锁定目标金额后,平仓原因仍应按权利金倍数模式标记,而非写死 fixed_usdt。"""
d = check_exits(
net_pnl=25.0,
exit_mode="premium_multiple",
net_profit_target=15.0,
premium_exit_multiple=1.0,
initial_premium=20.0,
locked_exit_target=20.0,
)
assert d.should_close is True
assert d.target == 20.0
assert d.reason == "premium_multiple"
def test_locked_target_ignores_setting_drift() -> None:
d = check_exits(
net_pnl=20.0,
+8 -8
View File
@@ -45,14 +45,14 @@ class _FakeClient:
return -1.5
def test_enrich_live_unrealized_entry_fee_times_two() -> None:
def test_enrich_live_unrealized_keeps_book_net_plus_funding() -> None:
base = {
"has_position": True,
"group_id": "G1",
"perp_side": "short",
"perp_upl": 1.0,
"option_upl": 5.0,
"est_close_fees": 9.9,
"est_close_fees": 0.8,
"net_pnl": -3.9,
}
@@ -69,12 +69,12 @@ def test_enrich_live_unrealized_entry_fee_times_two() -> None:
perp_side="short",
open_at_ms=1,
)
assert out["perp_upl"] == 8.0
assert out["perp_upl"] == 1.0 # 盘口可平
assert out["perp_upl_exchange"] == 8.0
assert out["option_upl"] == 5.0
assert out["fees_paid"] == 0.5
assert out["funding_usdt"] == -1.5
# 离场费按入场估算
assert out["est_close_fees"] == 0.5
# 8 + 5 - 0.5*2 + (-1.5) = 10.5
assert abs(out["net_pnl"] - 10.5) < 1e-9
assert out["pnl_source"] == "live_exchange"
assert out["est_close_fees"] == 0.8
# 盯盘净利 = 盘口净利 + 资金费,不用标记 UPL 覆盖
assert abs(out["net_pnl"] - (-3.9 - 1.5)) < 1e-9
assert out["pnl_source"] == "live_book_plus_funding"