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:
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user