diff --git a/lib/common/static/hedge_plan.js b/lib/common/static/hedge_plan.js index dd2ecf7..ea895c0 100644 --- a/lib/common/static/hedge_plan.js +++ b/lib/common/static/hedge_plan.js @@ -1166,12 +1166,9 @@ renderListStrikes(); renderTStrikes(); if (d.index_px) { - const idx = Number(d.index_px); - if ($("hp-target-up") && !$("hp-target-up").value) { - $("hp-target-up").value = String(Math.round(idx * 1.03)); - } - if ($("hp-target-down") && !$("hp-target-down").value) { - $("hp-target-down").value = String(Math.round(idx * 0.97)); + // 盈亏比默认2,不随指数自动改写 + if ($("hp-profit-rr") && !$("hp-profit-rr").value) { + $("hp-profit-rr").value = "2"; } } } @@ -1581,8 +1578,7 @@ if ($("hp-contracts")) $("hp-contracts").value = ""; if ($("hp-tp")) $("hp-tp").value = ""; if ($("hp-sl")) $("hp-sl").value = ""; - if ($("hp-target-up")) $("hp-target-up").value = ""; - if ($("hp-target-down")) $("hp-target-down").value = ""; + if ($("hp-profit-rr")) $("hp-profit-rr").value = "2"; if ($("hp-sel-inst")) $("hp-sel-inst").textContent = "—"; if ($("hp-premium-line")) $("hp-premium-line").textContent = ""; if ($("hp-oo-sheets-a")) { @@ -1618,16 +1614,12 @@ if (!matchesOoMoneyFilter(state.legA) || !matchesOoMoneyFilter(state.legB)) { throw new Error("期期两腿须为平值或虚值,不可选实值"); } - const up = Number(($("hp-target-up") && $("hp-target-up").value) || 0); - const down = Number(($("hp-target-down") && $("hp-target-down").value) || 0); - if (!up || !down) throw new Error("请填写上破与下破目标价"); - if (up <= down) throw new Error("上破目标价必须大于下破目标价"); + const rr = Number(($("hp-profit-rr") && $("hp-profit-rr").value) || 0); + if (!(rr > 0)) throw new Error("请填写盈亏比(须大于0,默认2)"); body = { plan_type: "options_options", - target_price_up: up, - target_price_down: down, - target_price: up, - index_px: indexPx() || (up + down) / 2, + profit_rr: rr, + index_px: indexPx() || 0, leg_a: legPayload(state.legA, ooSheets("hp-oo-sheets-a")), leg_b: legPayload(state.legB, ooSheets("hp-oo-sheets-b")), }; @@ -1719,22 +1711,28 @@ fmt(s.premium_paid) + (s.hedge_ratio_at_sl != null ? " · 止损对冲率 " + fmt(s.hedge_ratio_at_sl) + "%" : ""); } else { - const upTot = s.at_target_up_total != null ? s.at_target_up_total : s.at_target_total; - const dnTot = s.at_target_down_total; + const rrTarget = s.profit_rr != null ? s.profit_rr : null; let rrLine = ""; - if (s.rr_at_up != null || s.rr_at_down != null) { + if (rrTarget != null) { + rrLine = + " · 目标盈亏比 " + + fmt(rrTarget, 2) + + '(盈利金额/初始权利金)'; + } else if (s.rr_at_up != null || s.rr_at_down != null) { rrLine = " · 盈亏比 上破 " + fmtRr(s.rr_at_up) + - (dnTot != null ? " / 下破 " + fmtRr(s.rr_at_down) : "") + + (s.at_target_down_total != null ? " / 下破 " + fmtRr(s.rr_at_down) : "") + '(亏=全额保费 ' + fmt(s.rr_risk_premium != null ? s.rr_risk_premium : s.premium_paid) + ")"; } + const aTot = s.at_rr_a_full_total != null ? s.at_rr_a_full_total : s.at_target_up_total; + const bTot = s.at_rr_b_full_total != null ? s.at_rr_b_full_total : s.at_target_down_total; summary.innerHTML = - "上破 " + - fmtPnlHtml(upTot) + - (dnTot != null ? " · 下破 " + fmtPnlHtml(dnTot) : "") + + (rrTarget != null ? "腿A达标 " : "上破 ") + + fmtPnlHtml(aTot) + + (bTot != null ? (rrTarget != null ? " · 腿B达标 " : " · 下破 ") + fmtPnlHtml(bTot) : "") + " · 到期现价 " + fmtPnlHtml(s.expiry_flat_total) + " · 保费 " + @@ -2057,8 +2055,7 @@ "hp-tp", "hp-sl", "hp-sheets", - "hp-target-up", - "hp-target-down", + "hp-profit-rr", ]); if ($("hp-preview-btn")) $("hp-preview-btn").addEventListener("click", function () { @@ -2171,6 +2168,9 @@ if (p.plan_type === "perp_options") { return "止盈 " + fmt(p.tp) + " · 止损 " + fmt(p.sl); } + if (p.profit_rr != null && Number(p.profit_rr) > 0) { + return "盈亏比 " + fmt(p.profit_rr, 2); + } return "上破 " + fmt(p.target_price_up || p.target_price) + " · 下破 " + fmt(p.target_price_down || p.target_price); } @@ -2330,8 +2330,9 @@ target_win_leg: "期期平盈利腿", target_up_win_leg: "期期上破·平盈利腿", target_down_win_leg: "期期下破·平盈利腿", - oo_rest_closing: "期期全平·清残腿中", - oo_rest_closed: "期期全平·两腿已平", + profit_rr_win_leg: "期期盈亏比达标·平盈利腿", + oo_rest_closing: "期期残值平·清亏损腿中", + oo_rest_closed: "期期残值平·两腿已平", orphaned_after_tp: "止盈后持有至到期", orphaned_option_expiry: "残腿到期", hold_to_expiry: "持有至到期", @@ -2405,12 +2406,19 @@ fmt(p.perp_size, 4) + ""; } else { - html += - "
目标价 上破 " + - fmt(p.target_price_up || p.target_price) + - " · 下破 " + - fmt(p.target_price_down || p.target_price) + - "
"; + if (p.profit_rr != null && Number(p.profit_rr) > 0) { + html += + "
盈亏比 " + + fmt(p.profit_rr, 2) + + " (盈利金额/初始权利金)
"; + } else { + html += + "
目标价 上破 " + + fmt(p.target_price_up || p.target_price) + + " · 下破 " + + fmt(p.target_price_down || p.target_price) + + "
"; + } } html += "
权利金合计 " + @@ -2626,17 +2634,13 @@ if (!matchesOoMoneyFilter(state.legA) || !matchesOoMoneyFilter(state.legB)) { throw new Error("期期两腿须为平值或虚值,不可选实值"); } - const up = Number(($("hp-target-up") && $("hp-target-up").value) || 0); - const down = Number(($("hp-target-down") && $("hp-target-down").value) || 0); - if (!up || !down) throw new Error("请填写上破与下破目标价"); - if (up <= down) throw new Error("上破目标价必须大于下破目标价"); + const rr = Number(($("hp-profit-rr") && $("hp-profit-rr").value) || 0); + if (!(rr > 0)) throw new Error("请填写盈亏比(须大于0,默认2)"); body = { plan_type: "options_options", underlying: state.underlying, - target_price_up: up, - target_price_down: down, - target_price: up, - index_px: indexPx() || (up + down) / 2, + profit_rr: rr, + index_px: indexPx() || 0, oo_close_mode: state.ooCloseModeEnabled ? state.ooCloseMode : "hold_expiry", oo_sheets_mode: state.ooSheetsMode || "same_sheets", leg_a: legPayload(state.legA, ooSheets("hp-oo-sheets-a")), diff --git a/lib/common/static/options_panel.js b/lib/common/static/options_panel.js index 30e00f4..1fe04c8 100644 --- a/lib/common/static/options_panel.js +++ b/lib/common/static/options_panel.js @@ -1444,21 +1444,37 @@ function renderTargetDelegateRow(p) { const inst = p.inst_id || ""; const hedgeTarget = p.hedge_plan_target || null; - if (hedgeTarget && Number(hedgeTarget.target_index) > 0) { - const side = (p.opt_type || hedgeTarget.opt_type || "").toUpperCase() === "P" ? "Put ≤" : "Call ≥"; - return ( - '
' + - '对冲计划' + - '计划 #' + - hedgeTarget.plan_id + - " · " + - side + - " " + - fmt(hedgeTarget.target_index, 1) + - "" + - '进行中 · 由对冲计划监控,到位后仅平盈利腿' + - "
" - ); + if (hedgeTarget) { + const rr = hedgeTarget.profit_rr != null ? Number(hedgeTarget.profit_rr) : null; + if (rr != null && rr > 0) { + return ( + '
' + + '对冲计划' + + '计划 #' + + hedgeTarget.plan_id + + " · 盈亏比 " + + fmt(rr, 2) + + "" + + '进行中 · 达盈亏比仅平盈利腿;亏损腿残值平或到期平' + + "
" + ); + } + if (Number(hedgeTarget.target_index) > 0) { + const side = (p.opt_type || hedgeTarget.opt_type || "").toUpperCase() === "P" ? "Put ≤" : "Call ≥"; + return ( + '
' + + '对冲计划' + + '计划 #' + + hedgeTarget.plan_id + + " · " + + side + + " " + + fmt(hedgeTarget.target_index, 1) + + "" + + '进行中 · 由对冲计划监控,到位后仅平盈利腿' + + "
" + ); + } } const tgt = p.target_index != null && p.target_index !== "" ? Number(p.target_index) : null; const armed = tgt != null && Number.isFinite(tgt) && tgt > 0; @@ -1896,13 +1912,15 @@ }); } const hedgeTarget = p.hedge_plan_target; - if (hedgeTarget && hedgeTarget.target_index != null) { + if (hedgeTarget && (hedgeTarget.target_index != null || hedgeTarget.profit_rr != null)) { targets.push({ inst_id: p.inst_id, opt_type: p.opt_type || hedgeTarget.opt_type, target_index: hedgeTarget.target_index, + profit_rr: hedgeTarget.profit_rr, plan_id: hedgeTarget.plan_id, managed_by: hedgeTarget.managed_by, + exit_mode: hedgeTarget.exit_mode, }); } return targets; diff --git a/lib/common/static/options_review.js b/lib/common/static/options_review.js index d4d7e49..88d2540 100644 --- a/lib/common/static/options_review.js +++ b/lib/common/static/options_review.js @@ -76,8 +76,9 @@ target_win_leg: "期期平盈利腿", target_up_win_leg: "期期上破·平盈利腿", target_down_win_leg: "期期下破·平盈利腿", - oo_rest_closing: "期期全平·清残腿中", - oo_rest_closed: "期期全平·两腿已平", + profit_rr_win_leg: "期期盈亏比达标·平盈利腿", + oo_rest_closing: "期期残值平·清亏损腿中", + oo_rest_closed: "期期残值平·两腿已平", orphaned_after_tp: "止盈后持有至到期", orphaned_option_expiry: "残腿到期", hold_to_expiry: "持有至到期", diff --git a/lib/hedge_plan/hedge_plan_calc_lib.py b/lib/hedge_plan/hedge_plan_calc_lib.py index e508c21..c1aa86e 100644 --- a/lib/hedge_plan/hedge_plan_calc_lib.py +++ b/lib/hedge_plan/hedge_plan_calc_lib.py @@ -447,11 +447,15 @@ def build_options_options_preview( target_price: float | None = None, target_price_up: float | None = None, target_price_down: float | None = None, + profit_rr: float | None = None, index_px: float, leg_a: dict[str, Any], leg_b: dict[str, Any], ) -> dict[str, Any]: - """期期情景:上破/下破目标价 / 到期现价 / 最大保费损耗.""" + """期期情景:盈亏比达标 / 到期现价 / 最大保费损耗. + + 新口径优先 profit_rr(盈利金额/初始权利金);若未传则兼容旧上/下破目标价. + """ def _leg_pnl(leg: dict[str, Any], spot: float) -> float: return option_expiry_pnl( @@ -463,15 +467,109 @@ def build_options_options_preview( premium_paid=float(leg.get("premium_paid") or 0), ) + prem_a = float(leg_a.get("premium_paid") or 0) + prem_b = float(leg_b.get("premium_paid") or 0) + prem = prem_a + prem_b + rr = float(profit_rr) if profit_rr is not None else None + + # 新:盈亏比情景(不依赖指数上下破价) + 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 + b_at_a_full = -prem_b + b_at_a_res = -prem_b * 0.8 # 回收 20% + b_at_b = b_win + a_at_b_full = -prem_a + a_at_b_res = -prem_a * 0.8 + + a_flat = _leg_pnl(leg_a, index_px) + b_flat = _leg_pnl(leg_b, index_px) + flat_total = a_flat + b_flat + + return { + "plan_type": "options_options", + "premium_paid": round(prem, 6), + "profit_rr": rr, + "target_price": None, + "target_price_up": None, + "target_price_down": None, + "winner_at_up": "a", + "winner_at_down": "b", + "winner_at_target": "a", + "scenarios": [ + { + "id": "rr_leg_a_full", + "label": f"腿A达盈亏比{rr:g}(亏腿全损)", + "spot": None, + "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": "盈利腿按盈亏比兑现;亏损腿权利金全亏", + }, + { + "id": "rr_leg_b_full", + "label": f"腿B达盈亏比{rr:g}(亏腿全损)", + "spot": None, + "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": "盈利腿按盈亏比兑现;亏损腿权利金全亏", + }, + { + "id": "rr_leg_a_residual", + "label": f"腿A达盈亏比{rr:g}(亏腿残值20%)", + "spot": None, + "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%", + }, + { + "id": "expiry_flat", + "label": "到期·现价", + "spot": index_px, + "leg_a_pnl": round(a_flat, 4), + "leg_b_pnl": round(b_flat, 4), + "total": round(flat_total, 4), + "note": "无盈利则记总亏损结束" if flat_total <= 0 else "到期仍可能有净值", + }, + { + "id": "max_premium_loss", + "label": "最大保费损耗", + "spot": None, + "leg_a_pnl": round(-prem_a, 4), + "leg_b_pnl": round(-prem_b, 4), + "total": round(-prem, 4), + "note": "双腿权利金全部损失", + }, + ], + "summary": { + "profit_rr": rr, + "at_rr_a_full_total": round(a_at_a + b_at_a_full, 4), + "at_rr_b_full_total": round(a_at_b_full + b_at_b, 4), + "at_rr_a_residual_total": round(a_at_a + b_at_a_res, 4), + "at_target_up_total": round(a_at_a + b_at_a_full, 4), + "at_target_down_total": round(a_at_b_full + b_at_b, 4), + "at_target_total": round(a_at_a + b_at_a_full, 4), + "expiry_flat_total": round(flat_total, 4), + "premium_paid": round(prem, 6), + "expiry_is_loss": flat_total <= 0, + "rr_risk_premium": round(prem, 6), + "rr_at_up": round((a_at_a + b_at_a_full) / prem, 4) if prem > 0 else None, + "rr_at_down": round((a_at_b_full + b_at_b) / prem, 4) if prem > 0 else None, + }, + } + # 兼容旧单目标:若未传上下目标则用 target_price 填两边 up = target_price_up if target_price_up is not None else target_price down = target_price_down if target_price_down is not None else target_price if up is None or down is None: - raise ValueError("缺少上破/下破目标价") + raise ValueError("缺少盈亏比或上破/下破目标价") up_f = float(up) down_f = float(down) - prem = float(leg_a.get("premium_paid") or 0) + float(leg_b.get("premium_paid") or 0) a_up = _leg_pnl(leg_a, up_f) b_up = _leg_pnl(leg_b, up_f) at_up = a_up + b_up @@ -528,8 +626,8 @@ def build_options_options_preview( "id": "max_premium_loss", "label": "最大保费损耗", "spot": None, - "leg_a_pnl": round(-float(leg_a.get("premium_paid") or 0), 4), - "leg_b_pnl": round(-float(leg_b.get("premium_paid") or 0), 4), + "leg_a_pnl": round(-prem_a, 4), + "leg_b_pnl": round(-prem_b, 4), "total": round(-prem, 4), "note": "双腿权利金全部损失", }, diff --git a/lib/hedge_plan/hedge_plan_db.py b/lib/hedge_plan/hedge_plan_db.py index 299efa6..687bc47 100644 --- a/lib/hedge_plan/hedge_plan_db.py +++ b/lib/hedge_plan/hedge_plan_db.py @@ -72,7 +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") - # close_all=盈利腿平后清残腿;hold_expiry=残腿持有至到期(现状) + # 期期出场:盈利金额/初始权利金(默认2);有值则走盈亏比监控,旧单仍用上/下破价 + _ensure_column(conn, "hedge_plans", "profit_rr", "REAL") + # close_all=残值平(权利金≤初始20%且有买一);hold_expiry=残腿持有至到期 _ensure_column(conn, "hedge_plans", "oo_close_mode", "TEXT") # 永期「以期权为主」 _ensure_column(conn, "hedge_plans", "option_primary", "INTEGER") @@ -264,7 +266,7 @@ def attach_legs_to_plans(conn: sqlite3.Connection, plans: list[dict[str, Any]]) def active_options_targets_by_inst(conn: sqlite3.Connection) -> dict[str, dict[str, Any]]: - """返回由进行中「期期对冲」托管的期权目标位,仅供期权页只读展示。 + """返回由进行中「期期对冲」托管的期权目标,仅供期权页只读展示。 这些目标由 hedge_plan_monitor_lib 执行,绝不能写入 options_target_monitors, 否则两套监控会同时尝试平掉同一条期权腿。 @@ -272,7 +274,7 @@ def active_options_targets_by_inst(conn: sqlite3.Connection) -> dict[str, dict[s rows = conn.execute( """ SELECT p.id AS plan_id, p.underlying, p.target_price_up, p.target_price_down, - l.inst_id, l.opt_type + p.profit_rr, l.inst_id, l.opt_type FROM hedge_plans p JOIN hedge_plan_legs l ON l.plan_id = p.id WHERE p.plan_type = 'options_options' @@ -288,9 +290,24 @@ def active_options_targets_by_inst(conn: sqlite3.Connection) -> dict[str, dict[s row = dict(raw) inst_id = str(row.get("inst_id") or "") opt_type = str(row.get("opt_type") or "").upper() + if not inst_id or inst_id in out: + continue + profit_rr = _sf(row.get("profit_rr")) + if profit_rr is not None and profit_rr > 0: + out[inst_id] = { + "plan_id": int(row["plan_id"]), + "inst_id": inst_id, + "underlying": row.get("underlying"), + "opt_type": opt_type, + "profit_rr": profit_rr, + "target_index": None, + "exit_mode": "profit_rr", + "managed_by": "hedge_plan", + } + continue target = row.get("target_price_up") if opt_type == "C" else row.get("target_price_down") target_f = _sf(target) - if not inst_id or target_f is None or target_f <= 0 or inst_id in out: + if target_f is None or target_f <= 0: continue out[inst_id] = { "plan_id": int(row["plan_id"]), diff --git a/lib/hedge_plan/hedge_plan_monitor_lib.py b/lib/hedge_plan/hedge_plan_monitor_lib.py index 53addea..a2f4356 100644 --- a/lib/hedge_plan/hedge_plan_monitor_lib.py +++ b/lib/hedge_plan/hedge_plan_monitor_lib.py @@ -173,11 +173,11 @@ def _notify_end_reload(cfg: dict[str, Any], conn: Any, plan_id: int) -> None: def resolve_oo_rest_close_mode(plan: dict[str, Any]) -> str: - """盈利腿平后另一腿:close_all(全平) / hold_expiry(到期平). + """盈利腿平后另一腿:close_all(残值平) / hold_expiry(到期平). - 方案C关闭 → 强制到期平 - 计划未写 oo_close_mode(旧单) → 到期平,避免误清残腿 - - 新开仓默认写入 close_all + - 新开仓默认写入 close_all(残值平:权利金≤初始20%且有买一) """ if not _env_bool("HEDGE_PLAN_OO_CLOSE_MODE_ENABLED", True): return "hold_expiry" @@ -190,6 +190,12 @@ 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 + + def _oo_option_legs(legs: list[dict[str, Any]], *, statuses: tuple[str, ...]) -> list[dict[str, Any]]: out = [] for x in legs: @@ -200,6 +206,48 @@ def _oo_option_legs(legs: list[dict[str, Any]], *, statuses: tuple[str, ...]) -> return out +def _oo_quote_bid(cfg: dict[str, Any], inst_id: str) -> tuple[Optional[float], Optional[float]]: + quote_fn = cfg.get("quote_option_contract") + ex_opt = cfg.get("exchange_options") + if not callable(quote_fn) or ex_opt is None or not inst_id: + return None, None + try: + q = quote_fn(ex_opt, inst_id) + if not q.get("ok"): + return None, None + return _sf(q.get("bid")), _sf(q.get("bid_sz")) + except Exception: + return None, None + + +def _oo_leg_mark_value(leg: dict[str, Any], bid: Optional[float]) -> Optional[float]: + """买一可回收金额(USDC)= bid × 张数 × ct_mult.""" + b = _sf(bid) + if b is None or b < 0: + return None + sheets = float(leg.get("size") or 1) + ct = float(leg.get("ct_mult") or 0.01) + 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: + return None + value = _oo_leg_mark_value(leg, bid) + if value is None: + return None + return (value - premium) / premium + + +def _oo_resolve_profit_rr(plan: dict[str, Any]) -> Optional[float]: + rr = _sf(plan.get("profit_rr")) + if rr is not None and rr > 0: + return rr + return None + + def _finalize_oo_all_closed( cfg: dict[str, Any], conn: Any, plan: dict[str, Any], legs: list[dict[str, Any]], *, reason: str ) -> dict[str, Any]: @@ -985,7 +1033,12 @@ def _estimate_leg_close_pnl(leg: dict[str, Any], idx: Optional[float], bid: Opti def _tick_oo_close_rest( cfg: dict[str, Any], conn: Any, plan: dict[str, Any], legs: list[dict[str, Any]] ) -> Optional[dict[str, Any]]: - """盈利腿已平后:全平模式清残腿(无2×门控,买一失败则下轮重试).""" + """盈利腿已平后:残值平模式清亏损腿. + + 条件:买一回收 ≤ 初始权利金×20%,且买一有流动性;失败或未达条件则下轮重试. + """ + from lib.hedge_plan.hedge_plan_option_primary_lib import option_bid_liquidity_ok + if resolve_oo_rest_close_mode(plan) != "close_all": return None open_legs = _oo_option_legs(legs, statuses=("open",)) @@ -998,6 +1051,7 @@ def _tick_oo_close_rest( "target_win_leg", "target_up_win_leg", "target_down_win_leg", + "profit_rr_win_leg", "oo_rest_closing", "", ) @@ -1008,23 +1062,45 @@ def _tick_oo_close_rest( idx = _index_px(cfg, str(plan.get("underlying") or "ETH")) acted = False + waiting = False for leg in list(open_legs): - close_r = _sell_option( - cfg, inst_id=str(leg.get("inst_id") or ""), sheets=float(leg.get("size") or 1) - ) + inst_id = str(leg.get("inst_id") or "") + sheets = float(leg.get("size") or 1) + premium = float(leg.get("premium") or 0) + bid, bid_sz = _oo_quote_bid(cfg, inst_id) + value = _oo_leg_mark_value(leg, bid) + # 残值门槛:当前买一回收须 ≤ 初始权利金的 20% + if premium > 0: + if value is None: + waiting = True + continue + if value > premium * OO_LOSS_LEG_RESIDUAL_RATIO + 1e-12: + waiting = True + continue + liq_ok, liq_msg = option_bid_liquidity_ok(bid, bid_sz, need_sheets=sheets) + if not liq_ok: + waiting = True + update_plan(conn, int(plan["id"]), close_reason="oo_rest_closing") + return { + "plan_id": plan["id"], + "msg": "残值平等待买一流动性", + "detail": liq_msg, + "waiting": True, + } + close_r = _sell_option(cfg, inst_id=inst_id, sheets=sheets) if not close_r.get("ok"): notify_hedge( cfg, build_hedge_alert_message( - title="期期全平·残腿平仓失败(将重试)", + title="期期残值平·亏损腿平仓失败(将重试)", plan_id=plan.get("id"), detail=str(close_r.get("msg") or close_r), ), ) update_plan(conn, int(plan["id"]), close_reason="oo_rest_closing") return {"plan_id": plan["id"], "msg": "残腿平仓失败", "close": close_r, "retry": True} - bid = _sf(close_r.get("bid")) - est = _estimate_leg_close_pnl(leg, idx, bid) + bid_fill = _sf(close_r.get("bid")) or bid + est = _estimate_leg_close_pnl(leg, idx, bid_fill) pnl = _option_leg_pnl_after_close(cfg, leg, fallback=est) conn.execute( "UPDATE hedge_plan_legs SET status=?, close_reason=?, closed_at=?, realized_pnl=? WHERE id=?", @@ -1035,6 +1111,9 @@ def _tick_oo_close_rest( acted = True 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 None legs2 = get_plan_legs(conn, int(plan["id"])) still_open = _oo_option_legs(legs2, statuses=("open", "hold_to_expiry")) @@ -1046,10 +1125,118 @@ def _tick_oo_close_rest( ) +def _after_oo_winner_closed( + cfg: dict[str, Any], + conn: Any, + plan: dict[str, Any], + open_legs: list[dict[str, Any]], + best: dict[str, Any], + *, + reason: str, + extra: Optional[dict[str, Any]] = None, +) -> dict[str, Any]: + """盈利腿已平后:残值平同轮尝试 / 到期平标记 hold_to_expiry.""" + rest_mode = resolve_oo_rest_close_mode(plan) + update_plan(conn, int(plan["id"]), close_reason=reason) + mid = dict(plan) + mid["close_reason"] = reason + mid["status"] = "active" + mid["oo_close_mode"] = rest_mode + notify_plan_end(cfg, conn, mid) + + out: dict[str, Any] = { + "plan_id": plan["id"], + "close_reason": reason, + "closed_leg": best.get("id"), + "oo_close_mode": rest_mode, + } + if extra: + out.update(extra) + + if rest_mode == "close_all": + legs2 = get_plan_legs(conn, int(plan["id"])) + rest = _tick_oo_close_rest(cfg, conn, mid, legs2) + if rest: + out["rest"] = rest + return out + + for leg in open_legs: + if int(leg.get("id") or 0) == int(best.get("id") or 0): + continue + conn.execute( + "UPDATE hedge_plan_legs SET status=? WHERE id=?", + ("hold_to_expiry", leg["id"]), + ) + return out + + +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 + + 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) + if rr is None: + continue + value = _oo_leg_mark_value(leg, bid) or 0.0 + premium = float(leg.get("premium") or 0) + pnl = value - premium + ranked.append((rr, pnl, leg)) + if not ranked: + return None + ranked.sort(key=lambda x: x[0], reverse=True) + best_rr, best_pnl, best = ranked[0] + if best_rr + 1e-12 < float(rr_target) or best_pnl <= 0: + return None + + close_r = _sell_option( + cfg, inst_id=str(best.get("inst_id") or ""), sheets=float(best.get("size") or 1) + ) + if not close_r.get("ok"): + notify_hedge( + cfg, + build_hedge_alert_message( + title="期期平盈利腿失败", + plan_id=plan.get("id"), + detail=str(close_r.get("msg") or close_r), + ), + ) + return {"plan_id": plan["id"], "msg": "平盈利腿失败", "close": close_r} + + reason = "profit_rr_win_leg" + closed_pnl = _option_leg_pnl_after_close(cfg, best, fallback=float(best_pnl)) + conn.execute( + "UPDATE hedge_plan_legs SET status=?, close_reason=?, closed_at=?, realized_pnl=? WHERE id=?", + ("closed", reason, _now(), closed_pnl, best["id"]), + ) + idx = _index_px(cfg, str(plan.get("underlying") or "ETH")) + return _after_oo_winner_closed( + cfg, + conn, + plan, + open_legs, + best, + reason=reason, + extra={"profit_rr": best_rr, "rr_target": float(rr_target), "index": idx}, + ) + + def _tick_oo_target( cfg: dict[str, Any], conn: Any, plan: dict[str, Any], legs: list[dict[str, Any]] ) -> Optional[dict[str, Any]]: - """期期:触及上破或下破目标价时平盈利腿;按平仓模式处理另一腿.""" + """期期:优先按盈亏比平盈利腿;旧单无 profit_rr 时回退上/下破目标价.""" + rr_target = _oo_resolve_profit_rr(plan) + if rr_target is not None: + return _tick_oo_profit_rr(cfg, conn, plan, legs, rr_target=rr_target) + idx = _index_px(cfg, str(plan.get("underlying") or "ETH")) if idx is None: return None @@ -1065,10 +1252,8 @@ def _tick_oo_target( return None hit_side: Optional[str] = None - # 上破:现价接近或超过上破目标 if up is not None and idx >= up * 0.998: hit_side = "up" - # 下破:现价接近或低于下破目标 elif down is not None and idx <= down * 1.002: hit_side = "down" if not hit_side: @@ -1102,52 +1287,20 @@ def _tick_oo_target( ) return {"plan_id": plan["id"], "msg": "平盈利腿失败", "close": close_r} reason = "target_up_win_leg" if hit_side == "up" else "target_down_win_leg" - # 选腿用内在估算;落库优先交易所已实现盈亏 closed_pnl = _option_leg_pnl_after_close(cfg, best, fallback=float(best_pnl)) conn.execute( "UPDATE hedge_plan_legs SET status=?, close_reason=?, closed_at=?, realized_pnl=? WHERE id=?", ("closed", reason, _now(), closed_pnl, best["id"]), ) - rest_mode = resolve_oo_rest_close_mode(plan) - update_plan(conn, int(plan["id"]), close_reason=reason) - mid = dict(plan) - mid["close_reason"] = reason - mid["status"] = "active" - mid["oo_close_mode"] = rest_mode - notify_plan_end(cfg, conn, mid) - - # 全平:同轮尝试清残腿;失败则下轮 _tick_oo_close_rest 重试 - if rest_mode == "close_all": - legs2 = get_plan_legs(conn, int(plan["id"])) - rest = _tick_oo_close_rest(cfg, conn, mid, legs2) - out = { - "plan_id": plan["id"], - "close_reason": reason, - "hit_side": hit_side, - "closed_leg": best.get("id"), - "index": idx, - "oo_close_mode": rest_mode, - } - if rest: - out["rest"] = rest - return out - - # 到期平:显式标记残腿 hold_to_expiry - for leg in open_legs: - if int(leg.get("id") or 0) == int(best.get("id") or 0): - continue - conn.execute( - "UPDATE hedge_plan_legs SET status=? WHERE id=?", - ("hold_to_expiry", leg["id"]), - ) - return { - "plan_id": plan["id"], - "close_reason": reason, - "hit_side": hit_side, - "closed_leg": best.get("id"), - "index": idx, - "oo_close_mode": rest_mode, - } + return _after_oo_winner_closed( + cfg, + conn, + plan, + open_legs, + best, + reason=reason, + extra={"hit_side": hit_side, "index": idx}, + ) def _tick_oo_expiry( diff --git a/lib/hedge_plan/hedge_plan_notify_lib.py b/lib/hedge_plan/hedge_plan_notify_lib.py index 2e8ce7d..beefde5 100644 --- a/lib/hedge_plan/hedge_plan_notify_lib.py +++ b/lib/hedge_plan/hedge_plan_notify_lib.py @@ -46,13 +46,22 @@ def build_hedge_start_message(plan: dict[str, Any], *, legs: Optional[list[dict[ ] ) else: - lines.extend( - [ - f"🎯 上破:{_fmt(plan.get('target_price_up') or plan.get('target_price'))}" - f"|下破:{_fmt(plan.get('target_price_down') or plan.get('target_price'))}", - f"💎 期权保费合计:{_fmt(plan.get('premium_total'), 4)} USDC", - ] - ) + rr = plan.get("profit_rr") + if rr not in (None, ""): + lines.extend( + [ + f"🎯 盈亏比:{_fmt(rr)} (盈利金额/初始权利金)", + f"💎 期权保费合计:{_fmt(plan.get('premium_total'), 4)} USDC", + ] + ) + else: + lines.extend( + [ + f"🎯 上破:{_fmt(plan.get('target_price_up') or plan.get('target_price'))}" + f"|下破:{_fmt(plan.get('target_price_down') or plan.get('target_price'))}", + f"💎 期权保费合计:{_fmt(plan.get('premium_total'), 4)} USDC", + ] + ) if legs: for leg in legs: role = leg.get("leg_role") or "" @@ -81,8 +90,9 @@ def build_hedge_end_message(plan: dict[str, Any]) -> str: "target_win_leg": "期期已平盈利腿(中间态)", "target_up_win_leg": "期期上破·已平盈利腿", "target_down_win_leg": "期期下破·已平盈利腿", - "oo_rest_closing": "期期全平·清残腿中", - "oo_rest_closed": "期期全平·两腿已平", + "profit_rr_win_leg": "期期盈亏比达标·已平盈利腿", + "oo_rest_closing": "期期残值平·清亏损腿中", + "oo_rest_closed": "期期残值平·两腿已平", "oo_expiry_loss": "期期到期无盈利·总亏损", "oo_expiry_win": "期期到期仍盈利", "expiry": "到期收口", @@ -152,25 +162,37 @@ def notify_plan_end(cfg: dict[str, Any], conn: Any, plan: dict[str, Any]) -> boo "target_win_leg", "target_up_win_leg", "target_down_win_leg", + "profit_rr_win_leg", "oo_rest_closing", ) and (plan.get("status") or "") != "closed": - side = "上破" if "up" in str(plan.get("close_reason")) else ( - "下破" if "down" in str(plan.get("close_reason")) else "目标价" - ) + cr = str(plan.get("close_reason") or "") + if "profit_rr" in cr: + side = "盈亏比达标" + elif "up" in cr: + side = "上破" + elif "down" in cr: + side = "下破" + else: + side = "目标" mode = (plan.get("oo_close_mode") or "").strip().lower() - if mode in ("close_all", "全平"): - rest_txt = "另一腿将全平(买一清残腿,无2×门控,失败重试)" + if mode in ("close_all", "全平", "残值平"): + rest_txt = "另一腿残值平(权利金≤初始20%且有买一,失败重试)" else: rest_txt = "另一腿到期平(持有至到期结算)" + rr = plan.get("profit_rr") + if rr not in (None, ""): + detail = f"盈亏比 {_fmt(rr)} (盈利金额/初始权利金)" + else: + detail = ( + f"上破 {_fmt(plan.get('target_price_up') or plan.get('target_price'))}" + f"|下破 {_fmt(plan.get('target_price_down') or plan.get('target_price'))}" + ) notify_hedge( cfg, build_hedge_alert_message( title=f"期期{side}已平盈利腿 · {rest_txt}", plan_id=plan.get("id"), - detail=( - f"上破 {_fmt(plan.get('target_price_up') or plan.get('target_price'))}" - f"|下破 {_fmt(plan.get('target_price_down') or plan.get('target_price'))}" - ), + detail=detail, ), ) return True diff --git a/lib/hedge_plan/hedge_plan_orders_lib.py b/lib/hedge_plan/hedge_plan_orders_lib.py index d0180d4..f25b590 100644 --- a/lib/hedge_plan/hedge_plan_orders_lib.py +++ b/lib/hedge_plan/hedge_plan_orders_lib.py @@ -1146,20 +1146,30 @@ def validate_start_body(plan_type: str, body: dict[str, Any]) -> Optional[str]: b = body.get("leg_b") or {} if not a.get("inst_id") or not b.get("inst_id"): return "请选用两条期权腿" - up = body.get("target_price_up") - down = body.get("target_price_down") - legacy = body.get("target_price") - if up in (None, "") and legacy not in (None, ""): - up = legacy - if down in (None, "") and legacy not in (None, ""): - down = legacy - if up in (None, "") or down in (None, ""): - return "请填写上破与下破目标价" - try: - if float(up) <= float(down): - return "上破目标价必须大于下破目标价" - except (TypeError, ValueError): - return "目标价无效" + rr_raw = body.get("profit_rr") + if rr_raw not in (None, ""): + try: + rr = float(rr_raw) + except (TypeError, ValueError): + return "盈亏比无效" + if rr <= 0: + return "盈亏比须大于0" + else: + # 兼容旧上/下破 + up = body.get("target_price_up") + down = body.get("target_price_down") + legacy = body.get("target_price") + if up in (None, "") and legacy not in (None, ""): + up = legacy + if down in (None, "") and legacy not in (None, ""): + down = legacy + if up in (None, "") or down in (None, ""): + return "请填写盈亏比" + try: + if float(up) <= float(down): + return "上破目标价必须大于下破目标价" + except (TypeError, ValueError): + return "目标价无效" from lib.hedge_plan.hedge_plan_moneyness_lib import ( parse_strike_from_inst, validate_oo_legs_moneyness, @@ -1179,11 +1189,6 @@ def validate_start_body(plan_type: str, body: dict[str, Any]) -> Optional[str]: return {"opt_type": opt_type, "strike": strike} index_px = body.get("index_px") - if index_px in (None, ""): - try: - index_px = (float(up) + float(down)) / 2.0 - except (TypeError, ValueError): - index_px = None money_err = validate_oo_legs_moneyness( _leg_for_money(a), _leg_for_money(b), diff --git a/lib/hedge_plan/hedge_plan_register.py b/lib/hedge_plan/hedge_plan_register.py index 32958b3..4557a98 100644 --- a/lib/hedge_plan/hedge_plan_register.py +++ b/lib/hedge_plan/hedge_plan_register.py @@ -537,27 +537,36 @@ def _persist_oo(cfg: dict[str, Any], result: dict[str, Any], body: dict[str, Any premium = (float(a.get("premium") or 0) if a_ok else 0.0) + ( float(b.get("premium") or 0) if b_ok else 0.0 ) + rr_raw = body.get("profit_rr") + try: + profit_rr = float(rr_raw) if rr_raw not in (None, "") else 2.0 + except (TypeError, ValueError): + profit_rr = 2.0 + if profit_rr <= 0: + profit_rr = 2.0 + # 旧字段兼容:不再要求上/下破;有传则原样落库 + def _opt_float(key: str, *alts: str) -> float | None: + for k in (key, *alts): + v = body.get(k) + if v not in (None, ""): + try: + return float(v) + except (TypeError, ValueError): + continue + return None + + up_f = _opt_float("target_price_up", "target_price") + down_f = _opt_float("target_price_down", "target_price") plan_id = insert_plan( conn, { "plan_type": "options_options", "status": "partial" if is_partial else "active", "underlying": str(body.get("underlying") or "ETH").upper(), - "target_price": float( - body.get("target_price_up") - or body.get("target_price") - or 0 - ), - "target_price_up": float( - body.get("target_price_up") - or body.get("target_price") - or 0 - ), - "target_price_down": float( - body.get("target_price_down") - or body.get("target_price") - or 0 - ), + "target_price": up_f, + "target_price_up": up_f, + "target_price_down": down_f, + "profit_rr": profit_rr, "sizing_mode_at_open": load_position_sizing_mode(), "premium_total": premium, "oo_close_mode": _normalize_oo_close_mode(body.get("oo_close_mode")), @@ -1238,6 +1247,12 @@ def _preview_po(body: dict[str, Any]) -> dict[str, Any]: def _preview_oo(body: dict[str, Any]) -> dict[str, Any]: from lib.hedge_plan.hedge_plan_moneyness_lib import validate_oo_legs_moneyness + rr_raw = body.get("profit_rr") + profit_rr = None + if rr_raw not in (None, ""): + profit_rr = float(rr_raw) + if profit_rr <= 0: + raise ValueError("盈亏比须大于0") up = body.get("target_price_up") down = body.get("target_price_down") legacy = body.get("target_price") @@ -1245,13 +1260,19 @@ def _preview_oo(body: dict[str, Any]) -> dict[str, Any]: up = legacy if down in (None, "") and legacy not in (None, ""): down = legacy - if up in (None, "") or down in (None, ""): - raise ValueError("请填写上破与下破目标价") - up_f = float(up) - down_f = float(down) - if up_f <= down_f: + if profit_rr is None and (up in (None, "") or down in (None, "")): + raise ValueError("请填写盈亏比") + up_f = float(up) if up not in (None, "") else None + down_f = float(down) if down not in (None, "") else None + if profit_rr is None and up_f is not None and down_f is not None and up_f <= down_f: raise ValueError("上破目标价必须大于下破目标价") - index_px = float(body.get("index_px") or ((up_f + down_f) / 2)) + index_px = body.get("index_px") + if index_px in (None, ""): + if up_f is not None and down_f is not None: + index_px = (up_f + down_f) / 2 + else: + raise ValueError("缺少指数价格") + index_px = float(index_px) leg_a = body.get("leg_a") or {} leg_b = body.get("leg_b") or {} for name, leg in (("leg_a", leg_a), ("leg_b", leg_b)): @@ -1269,6 +1290,7 @@ def _preview_oo(body: dict[str, Any]) -> dict[str, Any]: if money_err: raise ValueError(money_err) return build_options_options_preview( + profit_rr=profit_rr, target_price_up=up_f, target_price_down=down_f, index_px=index_px, diff --git a/lib/hedge_plan/templates/hedge_plan_panel.html b/lib/hedge_plan/templates/hedge_plan_panel.html index de47b03..e025caf 100644 --- a/lib/hedge_plan/templates/hedge_plan_panel.html +++ b/lib/hedge_plan/templates/hedge_plan_panel.html @@ -213,7 +213,7 @@

账户:两腿都在期权账户。可用预算 = min(交易 USDC × 对冲缓冲 {{ '%.2f'|format(hedge_plan_budget_buffer|default(0.95)|float) }}, 单笔预算);可在 env「对冲预算缓冲比例」改。

下单:选 Call + Put 后「计算」再「启动」。启动会再拉卖一并按最新价重算张数,IOC 完全成交才算成功;资金不足可在右侧划转。

-

板块:左填上破/下破与张数模式(同张数/做多/做空);右 T 型选腿。两腿仅允许平值或虚值(禁实值)。「全平」= 盈利腿平后清另一腿;「到期平」= 另一腿持有至到期。

+

板块:左填盈亏比(盈利金额÷初始权利金,默认2)与张数模式(同张数/做多/做空);右 T 型选腿。两腿仅允许平值或虚值(禁实值)。出场:盈利腿达盈亏比即平;亏损腿「残值平」=权利金跌至初始20%且有买一时平,「到期平」=持有至到期。

@@ -221,8 +221,7 @@
- - + 指数 —
@@ -237,7 +236,7 @@
平仓
- +
diff --git a/lib/options/options_review_db.py b/lib/options/options_review_db.py index 7123a1d..f0db635 100644 --- a/lib/options/options_review_db.py +++ b/lib/options/options_review_db.py @@ -129,6 +129,7 @@ def init_options_review_tables(conn: sqlite3.Connection) -> None: _ensure_column(conn, "options_review_trades", "excluded_as_hedge_leg", "INTEGER DEFAULT 0") _ensure_column(conn, "options_review_trades", "target_price_up", "REAL") _ensure_column(conn, "options_review_trades", "target_price_down", "REAL") + _ensure_column(conn, "options_review_trades", "profit_rr", "REAL") def _ensure_column(conn: sqlite3.Connection, table: str, col: str, typedef: str) -> None: diff --git a/lib/options/options_review_lib.py b/lib/options/options_review_lib.py index 3353d6d..a57d88a 100644 --- a/lib/options/options_review_lib.py +++ b/lib/options/options_review_lib.py @@ -450,6 +450,7 @@ def upsert_hedge_plan_row( "target_price": _safe_float(plan.get("target_price")), "target_price_up": _safe_float(plan.get("target_price_up")), "target_price_down": _safe_float(plan.get("target_price_down")), + "profit_rr": _safe_float(plan.get("profit_rr")), "legs_json": _legs_json_from_plan(legs), } existing = conn.execute( diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js index 450dfba..62db67e 100644 --- a/manual_trading_hub/static/app.js +++ b/manual_trading_hub/static/app.js @@ -3930,11 +3930,17 @@ function renderOptionsTargetCell(target) { if (!target) return "—"; - const side = String(target.opt_type || "").toUpperCase() === "P" ? "Put≤" : "Call≥"; - const px = target.target_index != null ? fmt(target.target_index, 1) : "—"; if (target.managed_by === "hedge_plan") { + const rr = target.profit_rr != null ? Number(target.profit_rr) : null; + if (rr != null && rr > 0) { + return `对冲#${esc(target.plan_id)} 盈亏比 ${esc(fmt(rr, 2))}`; + } + const side = String(target.opt_type || "").toUpperCase() === "P" ? "Put≤" : "Call≥"; + const px = target.target_index != null ? fmt(target.target_index, 1) : "—"; return `对冲#${esc(target.plan_id)} ${esc(side)} ${esc(px)}`; } + const side = String(target.opt_type || "").toUpperCase() === "P" ? "Put≤" : "Call≥"; + const px = target.target_index != null ? fmt(target.target_index, 1) : "—"; return `${esc(side)} ${esc(px)}`; } diff --git a/tests/test_hedge_plan_calc.py b/tests/test_hedge_plan_calc.py index b3a4836..73943ea 100644 --- a/tests/test_hedge_plan_calc.py +++ b/tests/test_hedge_plan_calc.py @@ -102,20 +102,18 @@ class TestHedgePlanCalc(unittest.TestCase): a = {"opt_type": "C", "strike": 3300, "sheets": 1, "ct_mult": 0.01, "premium_paid": 5} b = {"opt_type": "P", "strike": 3100, "sheets": 1, "ct_mult": 0.01, "premium_paid": 5} p = build_options_options_preview( - target_price_up=3500, - target_price_down=3000, + profit_rr=2, index_px=3200, leg_a=a, leg_b=b, ) self.assertEqual(p["summary"]["premium_paid"], 10) self.assertTrue(p["summary"]["expiry_is_loss"]) - self.assertEqual(p["summary"]["rr_risk_premium"], 10) - self.assertIsNotNone(p["summary"]["rr_at_up"]) - self.assertAlmostEqual(p["summary"]["rr_at_up"], p["summary"]["at_target_up_total"] / 10, places=4) - self.assertEqual(len(p["scenarios"]), 4) - self.assertEqual(p["scenarios"][0]["id"], "target_up") - self.assertEqual(p["scenarios"][1]["id"], "target_down") + self.assertEqual(p["summary"]["profit_rr"], 2) + self.assertEqual(p["summary"]["at_rr_a_full_total"], 5) # 2*5 - 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") def test_oo_legacy_single_target_still_works(self): a = {"opt_type": "C", "strike": 3300, "sheets": 1, "ct_mult": 0.01, "premium_paid": 5} diff --git a/tests/test_hedge_plan_history_stats.py b/tests/test_hedge_plan_history_stats.py index ea451b0..e16dae4 100644 --- a/tests/test_hedge_plan_history_stats.py +++ b/tests/test_hedge_plan_history_stats.py @@ -155,6 +155,32 @@ class TestHedgeHistoryStats(unittest.TestCase): self.assertEqual(targets["ETH-USD_UM-260719-1850-P"]["target_index"], 1800) self.assertEqual(targets["ETH-USD_UM-260719-1890-C"]["managed_by"], "hedge_plan") + def test_active_options_targets_profit_rr(self): + conn = _mem() + pid = insert_plan( + conn, + { + "plan_type": "options_options", + "status": "active", + "underlying": "ETH", + "profit_rr": 2, + }, + ) + insert_leg( + conn, + { + "plan_id": pid, + "leg_role": "option_a", + "inst_id": "ETH-USD_UM-260719-1890-C", + "opt_type": "C", + "status": "open", + }, + ) + targets = active_options_targets_by_inst(conn) + self.assertEqual(targets["ETH-USD_UM-260719-1890-C"]["profit_rr"], 2) + self.assertEqual(targets["ETH-USD_UM-260719-1890-C"]["exit_mode"], "profit_rr") + self.assertIsNone(targets["ETH-USD_UM-260719-1890-C"]["target_index"]) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_hedge_plan_moneyness.py b/tests/test_hedge_plan_moneyness.py index 49b7e24..8c3599f 100644 --- a/tests/test_hedge_plan_moneyness.py +++ b/tests/test_hedge_plan_moneyness.py @@ -104,8 +104,7 @@ class TestHedgeMoneyness(unittest.TestCase): err = validate_start_body( "options_options", { - "target_price_up": 1900, - "target_price_down": 1700, + "profit_rr": 2, "index_px": 1800, "leg_a": {"inst_id": "ETH-USD-260731-1700-C", "opt_type": "C", "strike": 1700}, "leg_b": {"inst_id": "ETH-USD-260731-1900-P", "opt_type": "P", "strike": 1900}, diff --git a/tests/test_hedge_plan_orders.py b/tests/test_hedge_plan_orders.py index 6a8d875..b9c7384 100644 --- a/tests/test_hedge_plan_orders.py +++ b/tests/test_hedge_plan_orders.py @@ -169,9 +169,7 @@ class TestHedgePlanOrderPath(unittest.TestCase): "budget_buffer": 0.95, } body = { - "target_price": 1900, - "target_price_up": 1950, - "target_price_down": 1750, + "profit_rr": 2, "oo_sheets_mode": "same_sheets", "leg_a": {"inst_id": "A", "sheets": 1, "opt_type": "C"}, "leg_b": {"inst_id": "B", "sheets": 1, "opt_type": "P"},