Files
eth_hedge_sim/backend/app/strategy/exits.py
T
dekun bf3441537e 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>
2026-08-07 22:05:34 +08:00

128 lines
3.8 KiB
Python

from __future__ import annotations
import time
from dataclasses import dataclass
from typing import Any
EXIT_MODE_FIXED = "fixed_usdt"
EXIT_MODE_PREMIUM = "premium_multiple"
@dataclass(slots=True)
class ExitDecision:
should_close: bool
reason: str = ""
target: float = 0.0
def resolve_exit_target(
*,
exit_mode: str,
net_profit_target: float,
premium_exit_multiple: float,
initial_premium: float,
) -> tuple[float, str]:
"""返回 (出场目标金额 USDT, 模式标记)。"""
mode = (exit_mode or EXIT_MODE_FIXED).strip().lower()
if mode == EXIT_MODE_PREMIUM:
mult = max(0.0, float(premium_exit_multiple))
return float(initial_premium) * mult, EXIT_MODE_PREMIUM
return float(net_profit_target), EXIT_MODE_FIXED
def lock_trade_exit_target(
db: Any,
*,
group_id: str,
initial_premium: float,
) -> float:
"""
开仓成交后锁定本组成交出场目标到 groups/positions。
持仓期间盯盘与展示均用该值,不再跟随时价重算以损定仓/出场。
"""
from ..config import get_settings
from ..sim.ledger import Ledger
s = get_settings()
ledger = Ledger(db)
exit_mode = ledger.get_setting_str("exit_mode", s.exit_mode) or EXIT_MODE_FIXED
net_target = float(
ledger.get_setting_float("net_profit_target", s.net_profit_target)
or s.net_profit_target
)
prem_mult = float(
ledger.get_setting_float("premium_exit_multiple", s.premium_exit_multiple)
or s.premium_exit_multiple
)
target, _mode = resolve_exit_target(
exit_mode=str(exit_mode),
net_profit_target=net_target,
premium_exit_multiple=prem_mult,
initial_premium=float(initial_premium or 0),
)
target = float(target)
db.execute(
"UPDATE groups SET exit_target_usdt=? WHERE group_id=?",
(target, group_id),
)
db.execute(
"UPDATE positions SET exit_target_usdt=? WHERE id=1",
(target,),
)
return target
def read_locked_exit_target(pos: dict[str, Any] | None) -> float | None:
"""持仓行上的锁定目标;无则 None(旧仓回退设置值)。"""
if not pos:
return None
v = pos.get("exit_target_usdt")
if v is None or v == "":
return None
try:
f = float(v)
except (TypeError, ValueError):
return None
return f if f > 0 else None
def check_expiry_close(
*,
expiry_ms: int | None,
now_ms: int | None = None,
) -> ExitDecision:
"""期权到期时刻(含)→ 强制全平。"""
if expiry_ms is None:
return ExitDecision(False, "", 0.0)
now = int(time.time() * 1000) if now_ms is None else int(now_ms)
if now >= int(expiry_ms):
return ExitDecision(True, "expiry", 0.0)
return ExitDecision(False, "", 0.0)
def check_exits(
*,
net_pnl: float,
exit_mode: str,
net_profit_target: float,
premium_exit_multiple: float,
initial_premium: float,
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)
else:
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)
return ExitDecision(False, "", target)