期期出场改盈亏比:达目标平盈利腿,亏损腿残值20%或到期平
将上/下破目标价替换为盈亏比(盈利金额/初始权利金,默认2);残值平需买一流动性且权利金≤初始20%。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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) +
|
||||
'<span class="muted">(盈利金额/初始权利金)</span>';
|
||||
} 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) : "") +
|
||||
'<span class="muted">(亏=全额保费 ' +
|
||||
fmt(s.rr_risk_premium != null ? s.rr_risk_premium : s.premium_paid) +
|
||||
")</span>";
|
||||
}
|
||||
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) +
|
||||
"</div>";
|
||||
} else {
|
||||
html +=
|
||||
"<div><span class=\"muted\">目标价</span> 上破 " +
|
||||
fmt(p.target_price_up || p.target_price) +
|
||||
" · 下破 " +
|
||||
fmt(p.target_price_down || p.target_price) +
|
||||
"</div>";
|
||||
if (p.profit_rr != null && Number(p.profit_rr) > 0) {
|
||||
html +=
|
||||
"<div><span class=\"muted\">盈亏比</span> " +
|
||||
fmt(p.profit_rr, 2) +
|
||||
" <span class=\"muted\">(盈利金额/初始权利金)</span></div>";
|
||||
} else {
|
||||
html +=
|
||||
"<div><span class=\"muted\">目标价</span> 上破 " +
|
||||
fmt(p.target_price_up || p.target_price) +
|
||||
" · 下破 " +
|
||||
fmt(p.target_price_down || p.target_price) +
|
||||
"</div>";
|
||||
}
|
||||
}
|
||||
html +=
|
||||
"<div><span class=\"muted\">权利金合计</span> " +
|
||||
@@ -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")),
|
||||
|
||||
@@ -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 (
|
||||
'<div class="opt-target-row opt-target-row--managed">' +
|
||||
'<span class="opt-target-row-label">对冲计划</span>' +
|
||||
'<span class="opt-target-armed">计划 #' +
|
||||
hedgeTarget.plan_id +
|
||||
" · " +
|
||||
side +
|
||||
" " +
|
||||
fmt(hedgeTarget.target_index, 1) +
|
||||
"</span>" +
|
||||
'<span class="muted opt-target-row-hint">进行中 · 由对冲计划监控,到位后仅平盈利腿</span>' +
|
||||
"</div>"
|
||||
);
|
||||
if (hedgeTarget) {
|
||||
const rr = hedgeTarget.profit_rr != null ? Number(hedgeTarget.profit_rr) : null;
|
||||
if (rr != null && rr > 0) {
|
||||
return (
|
||||
'<div class="opt-target-row opt-target-row--managed">' +
|
||||
'<span class="opt-target-row-label">对冲计划</span>' +
|
||||
'<span class="opt-target-armed">计划 #' +
|
||||
hedgeTarget.plan_id +
|
||||
" · 盈亏比 " +
|
||||
fmt(rr, 2) +
|
||||
"</span>" +
|
||||
'<span class="muted opt-target-row-hint">进行中 · 达盈亏比仅平盈利腿;亏损腿残值平或到期平</span>' +
|
||||
"</div>"
|
||||
);
|
||||
}
|
||||
if (Number(hedgeTarget.target_index) > 0) {
|
||||
const side = (p.opt_type || hedgeTarget.opt_type || "").toUpperCase() === "P" ? "Put ≤" : "Call ≥";
|
||||
return (
|
||||
'<div class="opt-target-row opt-target-row--managed">' +
|
||||
'<span class="opt-target-row-label">对冲计划</span>' +
|
||||
'<span class="opt-target-armed">计划 #' +
|
||||
hedgeTarget.plan_id +
|
||||
" · " +
|
||||
side +
|
||||
" " +
|
||||
fmt(hedgeTarget.target_index, 1) +
|
||||
"</span>" +
|
||||
'<span class="muted opt-target-row-hint">进行中 · 由对冲计划监控,到位后仅平盈利腿</span>' +
|
||||
"</div>"
|
||||
);
|
||||
}
|
||||
}
|
||||
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;
|
||||
|
||||
@@ -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: "持有至到期",
|
||||
|
||||
@@ -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": "双腿权利金全部损失",
|
||||
},
|
||||
|
||||
@@ -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"]),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>:左填上破/下破与张数模式(同张数/做多/做空);右 T 型选腿。<strong>两腿仅允许平值或虚值</strong>(禁实值)。「全平」= 盈利腿平后清另一腿;「到期平」= 另一腿持有至到期。</p>
|
||||
<p><strong>板块</strong>:左填<strong>盈亏比</strong>(盈利金额÷初始权利金,默认2)与张数模式(同张数/做多/做空);右 T 型选腿。<strong>两腿仅允许平值或虚值</strong>(禁实值)。出场:盈利腿达盈亏比即平;亏损腿「残值平」=权利金跌至初始20%且有买一时平,「到期平」=持有至到期。</p>
|
||||
</div>
|
||||
</details>
|
||||
<div class="form-row hp-uly-row">
|
||||
@@ -221,8 +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>上破目标 <input type="number" step="any" id="hp-target-up" placeholder="向上突破" autocomplete="off" inputmode="decimal" data-lpignore="true" data-1p-ignore="true" data-form-type="other" /></label>
|
||||
<label>下破目标 <input type="number" step="any" id="hp-target-down" placeholder="向下突破" 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">
|
||||
@@ -237,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="盈利腿平后立刻买一清另一腿(无2×,失败重试)"><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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -3930,11 +3930,17 @@
|
||||
|
||||
function renderOptionsTargetCell(target) {
|
||||
if (!target) return "<td>—</td>";
|
||||
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 `<td class="hub-opt-target-cell is-on is-hedge" title="由对冲计划监控">对冲#${esc(target.plan_id)} 盈亏比 ${esc(fmt(rr, 2))}</td>`;
|
||||
}
|
||||
const side = String(target.opt_type || "").toUpperCase() === "P" ? "Put≤" : "Call≥";
|
||||
const px = target.target_index != null ? fmt(target.target_index, 1) : "—";
|
||||
return `<td class="hub-opt-target-cell is-on is-hedge" title="由对冲计划监控">对冲#${esc(target.plan_id)} ${esc(side)} ${esc(px)}</td>`;
|
||||
}
|
||||
const side = String(target.opt_type || "").toUpperCase() === "P" ? "Put≤" : "Call≥";
|
||||
const px = target.target_index != null ? fmt(target.target_index, 1) : "—";
|
||||
return `<td class="hub-opt-target-cell is-on" title="目标监控">${esc(side)} ${esc(px)}</td>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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"},
|
||||
|
||||
Reference in New Issue
Block a user