修正期期盈亏比口径:按总权利金计算,残值按本合约

盈利腿触发改为盈利金额/总权利金;亏损腿残值20%仍相对该合约自身权利金。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-11 16:51:50 +08:00
parent c5d3d9d6c1
commit a1bf760a28
8 changed files with 58 additions and 35 deletions
+2 -2
View File
@@ -1717,7 +1717,7 @@
rrLine =
" · 目标盈亏比 " +
fmt(rrTarget, 2) +
'<span class="muted">(盈利金额/初始权利金)</span>';
'<span class="muted">(盈利金额/权利金)</span>';
} else if (s.rr_at_up != null || s.rr_at_down != null) {
rrLine =
" · 盈亏比 上破 " +
@@ -2410,7 +2410,7 @@
html +=
"<div><span class=\"muted\">盈亏比</span> " +
fmt(p.profit_rr, 2) +
" <span class=\"muted\">(盈利金额/初始权利金)</span></div>";
" <span class=\"muted\">(盈利金额/权利金)</span></div>";
} else {
html +=
"<div><span class=\"muted\">目标价</span> 上破 " +
+1 -1
View File
@@ -1455,7 +1455,7 @@
" · 盈亏比 " +
fmt(rr, 2) +
"</span>" +
'<span class="muted opt-target-row-hint">进行中 · 盈亏比仅平盈利腿;亏损腿残值平或到期平</span>' +
'<span class="muted opt-target-row-hint">进行中 · 盈利达总权利金×盈亏比仅平盈利腿;亏损腿按本合约残值平或到期平</span>' +
"</div>"
);
}
+10 -10
View File
@@ -454,7 +454,8 @@ def build_options_options_preview(
) -> dict[str, Any]:
"""期期情景:盈亏比达标 / 到期现价 / 最大保费损耗.
新口径优先 profit_rr(盈利金额/初始权利金);若未传则兼容旧上/下破目标价.
新口径优先 profit_rr(盈利金额/权利金);若未传则兼容旧上/下破目标价.
残值按亏损腿本合约权利金的 20% 计.
"""
def _leg_pnl(leg: dict[str, Any], spot: float) -> float:
@@ -474,13 +475,12 @@ def build_options_options_preview(
# 新:盈亏比情景(不依赖指数上下破价)
if rr is not None and rr > 0:
# 盈利腿达 RR:盈利 = rr × 该腿权利金;亏损腿按全亏 / 残值20%回收两种
a_win = rr * prem_a
b_win = rr * prem_b
a_at_a = a_win
# 盈利腿达 RR:盈利金额 = rr × 权利金;亏损腿按全亏 / 本合约残值20%回收
win_profit = rr * prem
a_at_a = win_profit
b_at_a_full = -prem_b
b_at_a_res = -prem_b * 0.8 # 回收 20%
b_at_b = b_win
b_at_a_res = -prem_b * 0.8 # 本合约回收 20%
b_at_b = win_profit
a_at_b_full = -prem_a
a_at_b_res = -prem_a * 0.8
@@ -506,7 +506,7 @@ def build_options_options_preview(
"leg_a_pnl": round(a_at_a, 4),
"leg_b_pnl": round(b_at_a_full, 4),
"total": round(a_at_a + b_at_a_full, 4),
"note": "盈利腿按盈亏比兑现;亏损腿权利金全亏",
"note": "盈利=总权利金×盈亏比;亏损腿本合约全亏",
},
{
"id": "rr_leg_b_full",
@@ -515,7 +515,7 @@ def build_options_options_preview(
"leg_a_pnl": round(a_at_b_full, 4),
"leg_b_pnl": round(b_at_b, 4),
"total": round(a_at_b_full + b_at_b, 4),
"note": "盈利腿按盈亏比兑现;亏损腿权利金全亏",
"note": "盈利=总权利金×盈亏比;亏损腿本合约全亏",
},
{
"id": "rr_leg_a_residual",
@@ -524,7 +524,7 @@ def build_options_options_preview(
"leg_a_pnl": round(a_at_a, 4),
"leg_b_pnl": round(b_at_a_res, 4),
"total": round(a_at_a + b_at_a_res, 4),
"note": "亏损腿买一回收约初始权利金20%",
"note": "亏损腿买一回收约本合约权利金20%",
},
{
"id": "expiry_flat",
+2 -2
View File
@@ -72,9 +72,9 @@ def init_hedge_plan_tables(conn: sqlite3.Connection) -> None:
)
_ensure_column(conn, "hedge_plans", "target_price_up", "REAL")
_ensure_column(conn, "hedge_plans", "target_price_down", "REAL")
# 期期出场:盈利金额/初始权利金(默认2);有值则走盈亏比监控,旧单仍用上/下破价
# 期期出场:盈利金额/权利金(默认2);有值则走盈亏比监控,旧单仍用上/下破价
_ensure_column(conn, "hedge_plans", "profit_rr", "REAL")
# close_all=残值平(权利金≤初始20%且有买一);hold_expiry=残腿持有至到期
# close_all=残值平(本合约权利金≤20%且有买一);hold_expiry=残腿持有至到期
_ensure_column(conn, "hedge_plans", "oo_close_mode", "TEXT")
# 永期「以期权为主」
_ensure_column(conn, "hedge_plans", "option_primary", "INTEGER")
+36 -13
View File
@@ -190,9 +190,9 @@ def resolve_oo_rest_close_mode(plan: dict[str, Any]) -> str:
return "close_all"
# 期期亏损腿残值平:当前买一回收 ≤ 初始权利金 × 该比例
# 期期亏损腿残值平:当前买一回收 ≤ 本合约初始权利金 × 该比例
OO_LOSS_LEG_RESIDUAL_RATIO = 0.20
# 期期默认盈亏比:盈利金额 / 初始权利金
# 期期默认盈亏比:盈利金额 / 权利金
OO_DEFAULT_PROFIT_RR = 2.0
@@ -230,15 +230,30 @@ def _oo_leg_mark_value(leg: dict[str, Any], bid: Optional[float]) -> Optional[fl
return float(b) * sheets * ct
def _oo_leg_profit_rr(leg: dict[str, Any], bid: Optional[float]) -> Optional[float]:
"""盈亏比 = 盈利金额 / 初始权利金;盈利金额 = 买一回收 − 初始权利金."""
premium = float(leg.get("premium") or 0)
if premium <= 0:
def _oo_plan_premium_total(plan: dict[str, Any], legs: list[dict[str, Any]]) -> float:
"""双腿总权利金:优先计划字段,否则对期权腿 premium 求和."""
total = _sf(plan.get("premium_total"))
if total is not None and total > 0:
return float(total)
s = 0.0
for leg in legs:
if not str(leg.get("leg_role") or "").startswith("option"):
continue
s += float(leg.get("premium") or 0)
return s
def _oo_leg_profit_rr(
leg: dict[str, Any], bid: Optional[float], *, total_premium: float
) -> Optional[float]:
"""盈亏比 = 该腿盈利金额 / 总权利金;盈利金额 = 买一回收 − 本腿权利金."""
if total_premium <= 0:
return None
leg_prem = float(leg.get("premium") or 0)
value = _oo_leg_mark_value(leg, bid)
if value is None:
return None
return (value - premium) / premium
return (value - leg_prem) / total_premium
def _oo_resolve_profit_rr(plan: dict[str, Any]) -> Optional[float]:
@@ -1035,7 +1050,7 @@ def _tick_oo_close_rest(
) -> Optional[dict[str, Any]]:
"""盈利腿已平后:残值平模式清亏损腿.
条件:买一回收 ≤ 初始权利金×20%,且买一有流动性;失败或未达条件则下轮重试.
条件:买一回收 ≤ 本合约初始权利金×20%,且买一有流动性;失败或未达条件则下轮重试.
"""
from lib.hedge_plan.hedge_plan_option_primary_lib import option_bid_liquidity_ok
@@ -1069,7 +1084,7 @@ def _tick_oo_close_rest(
premium = float(leg.get("premium") or 0)
bid, bid_sz = _oo_quote_bid(cfg, inst_id)
value = _oo_leg_mark_value(leg, bid)
# 残值门槛:当前买一回收须 ≤ 初始权利金的 20%
# 残值门槛:相对本合约初始权利金,买一回收须 ≤ 20%
if premium > 0:
if value is None:
waiting = True
@@ -1113,7 +1128,7 @@ def _tick_oo_close_rest(
if not acted:
if waiting:
update_plan(conn, int(plan["id"]), close_reason="oo_rest_closing")
return {"plan_id": plan["id"], "msg": "残值平等待权利金≤20%", "waiting": True}
return {"plan_id": plan["id"], "msg": "残值平等待本合约权利金≤20%", "waiting": True}
return None
legs2 = get_plan_legs(conn, int(plan["id"]))
still_open = _oo_option_legs(legs2, statuses=("open", "hold_to_expiry"))
@@ -1173,17 +1188,20 @@ def _after_oo_winner_closed(
def _tick_oo_profit_rr(
cfg: dict[str, Any], conn: Any, plan: dict[str, Any], legs: list[dict[str, Any]], *, rr_target: float
) -> Optional[dict[str, Any]]:
"""期期:任一开仓腿盈亏比(盈利金额/初始权利金)达目标 → 平盈利腿."""
"""期期:任一开仓腿盈亏比(该腿盈利金额/权利金)达目标 → 平盈利腿."""
if not _env_bool("HEDGE_PLAN_OO_CLOSE_WINNER_ONLY", True):
return None
open_legs = _oo_option_legs(legs, statuses=("open",))
if len(open_legs) < 2:
return None
total_prem = _oo_plan_premium_total(plan, legs)
if total_prem <= 0:
return None
ranked: list[tuple[float, float, dict[str, Any]]] = []
for leg in open_legs:
bid, _bid_sz = _oo_quote_bid(cfg, str(leg.get("inst_id") or ""))
rr = _oo_leg_profit_rr(leg, bid)
rr = _oo_leg_profit_rr(leg, bid, total_premium=total_prem)
if rr is None:
continue
value = _oo_leg_mark_value(leg, bid) or 0.0
@@ -1225,7 +1243,12 @@ def _tick_oo_profit_rr(
open_legs,
best,
reason=reason,
extra={"profit_rr": best_rr, "rr_target": float(rr_target), "index": idx},
extra={
"profit_rr": best_rr,
"rr_target": float(rr_target),
"total_premium": total_prem,
"index": idx,
},
)
+3 -3
View File
@@ -50,7 +50,7 @@ def build_hedge_start_message(plan: dict[str, Any], *, legs: Optional[list[dict[
if rr not in (None, ""):
lines.extend(
[
f"🎯 盈亏比:{_fmt(rr)} (盈利金额/初始权利金)",
f"🎯 盈亏比:{_fmt(rr)} (盈利金额/权利金)",
f"💎 期权保费合计:{_fmt(plan.get('premium_total'), 4)} USDC",
]
)
@@ -176,12 +176,12 @@ def notify_plan_end(cfg: dict[str, Any], conn: Any, plan: dict[str, Any]) -> boo
side = "目标"
mode = (plan.get("oo_close_mode") or "").strip().lower()
if mode in ("close_all", "全平", "残值平"):
rest_txt = "另一腿残值平(权利金≤初始20%且有买一,失败重试)"
rest_txt = "另一腿残值平(本合约权利金≤20%且有买一,失败重试)"
else:
rest_txt = "另一腿到期平(持有至到期结算)"
rr = plan.get("profit_rr")
if rr not in (None, ""):
detail = f"盈亏比 {_fmt(rr)} (盈利金额/初始权利金)"
detail = f"盈亏比 {_fmt(rr)} (盈利金额/权利金)"
else:
detail = (
f"上破 {_fmt(plan.get('target_price_up') or plan.get('target_price'))}"
@@ -213,7 +213,7 @@
<div class="tip-collapse-body rule-tip">
<p><strong>账户</strong>:两腿都在<strong>期权账户</strong>。可用预算 = min(交易 USDC × 对冲缓冲 <strong id="hp-oo-buf-ratio">{{ '%.2f'|format(hedge_plan_budget_buffer|default(0.95)|float) }}</strong>, 单笔预算);可在 env「对冲预算缓冲比例」改。</p>
<p><strong>下单</strong>:选 Call + Put 后「计算」再「启动」。启动会再拉卖一并按最新价重算张数,IOC 完全成交才算成功;资金不足可在右侧划转。</p>
<p><strong>板块</strong>:左填<strong>盈亏比</strong>(盈利金额÷初始权利金,默认2)与张数模式(同张数/做多/做空);右 T 型选腿。<strong>两腿仅允许平值或虚值</strong>(禁实值)。出场:盈利腿达盈亏比即平;亏损腿「残值平」=权利金跌至初始20%且有买一时平,「到期平」=持有至到期。</p>
<p><strong>板块</strong>:左填<strong>盈亏比</strong>(盈利金额÷权利金,默认2)与张数模式(同张数/做多/做空);右 T 型选腿。<strong>两腿仅允许平值或虚值</strong>(禁实值)。出场:盈利腿达盈亏比即平;亏损腿「残值平」=本合约权利金跌至20%且有买一时平,「到期平」=持有至到期。</p>
</div>
</details>
<div class="form-row hp-uly-row">
@@ -221,7 +221,7 @@
<button type="button" class="btn-secondary hp-uly-btn-oo" data-uly="BTC">BTC</button>
</div>
<div class="form-row hp-target-row hp-oo-target-row">
<label title="盈利金额 / 初始权利金">盈亏比 <input type="number" step="0.1" min="0.1" id="hp-profit-rr" value="2" placeholder="默认2" autocomplete="off" inputmode="decimal" data-lpignore="true" data-1p-ignore="true" data-form-type="other" /></label>
<label title="盈利金额 / 权利金">盈亏比 <input type="number" step="0.1" min="0.1" id="hp-profit-rr" value="2" placeholder="默认2" autocomplete="off" inputmode="decimal" data-lpignore="true" data-1p-ignore="true" data-form-type="other" /></label>
<span id="hp-oo-index" class="hp-oo-index" aria-live="polite">指数 —</span>
</div>
<div class="hp-oo-controls">
@@ -236,7 +236,7 @@
<div class="hp-oo-ctrl" id="hp-oo-close-mode-row">
<span class="hp-oo-ctrl-lab" title="仅控制盈利腿平掉后的另一腿">平仓</span>
<div class="hp-oo-seg" role="group" aria-label="平仓模式">
<button type="button" class="btn-secondary hp-oo-close-mode is-selected" data-oo-close="close_all" title="盈利腿平后:亏损腿权利金跌至初始20%且有买一时平掉(失败重试)"><span class="hp-oo-check" aria-hidden="true"></span>残值平</button>
<button type="button" class="btn-secondary hp-oo-close-mode is-selected" data-oo-close="close_all" title="盈利腿平后:亏损腿本合约权利金跌至20%且有买一时平掉(失败重试)"><span class="hp-oo-check" aria-hidden="true"></span>残值平</button>
<button type="button" class="btn-secondary hp-oo-close-mode" data-oo-close="hold_expiry" title="盈利腿平后另一腿持有至到期"><span class="hp-oo-check" aria-hidden="true"></span>到期平</button>
</div>
</div>
+1 -1
View File
@@ -110,7 +110,7 @@ class TestHedgePlanCalc(unittest.TestCase):
self.assertEqual(p["summary"]["premium_paid"], 10)
self.assertTrue(p["summary"]["expiry_is_loss"])
self.assertEqual(p["summary"]["profit_rr"], 2)
self.assertEqual(p["summary"]["at_rr_a_full_total"], 5) # 2*5 - 5
self.assertEqual(p["summary"]["at_rr_a_full_total"], 15) # 盈利=2*10, 亏腿-5
self.assertEqual(len(p["scenarios"]), 5)
self.assertEqual(p["scenarios"][0]["id"], "rr_leg_a_full")
self.assertEqual(p["scenarios"][1]["id"], "rr_leg_b_full")