feat(hedge): replace OO breakout targets with premium profit RR
期期改用盈亏比×权利金止盈(默认2);不达标持有至到期。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1165,14 +1165,9 @@
|
|||||||
fillExpSelect($("hp-oo-exp-select"), d);
|
fillExpSelect($("hp-oo-exp-select"), d);
|
||||||
renderListStrikes();
|
renderListStrikes();
|
||||||
renderTStrikes();
|
renderTStrikes();
|
||||||
if (d.index_px) {
|
// 期期盈亏比默认 2,不再用指数自动填上破/下破
|
||||||
const idx = Number(d.index_px);
|
if ($("hp-oo-rr") && !$("hp-oo-rr").value) {
|
||||||
if ($("hp-target-up") && !$("hp-target-up").value) {
|
$("hp-oo-rr").value = "2";
|
||||||
$("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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1581,8 +1576,7 @@
|
|||||||
if ($("hp-contracts")) $("hp-contracts").value = "";
|
if ($("hp-contracts")) $("hp-contracts").value = "";
|
||||||
if ($("hp-tp")) $("hp-tp").value = "";
|
if ($("hp-tp")) $("hp-tp").value = "";
|
||||||
if ($("hp-sl")) $("hp-sl").value = "";
|
if ($("hp-sl")) $("hp-sl").value = "";
|
||||||
if ($("hp-target-up")) $("hp-target-up").value = "";
|
if ($("hp-oo-rr")) $("hp-oo-rr").value = "2";
|
||||||
if ($("hp-target-down")) $("hp-target-down").value = "";
|
|
||||||
if ($("hp-sel-inst")) $("hp-sel-inst").textContent = "—";
|
if ($("hp-sel-inst")) $("hp-sel-inst").textContent = "—";
|
||||||
if ($("hp-premium-line")) $("hp-premium-line").textContent = "";
|
if ($("hp-premium-line")) $("hp-premium-line").textContent = "";
|
||||||
if ($("hp-oo-sheets-a")) {
|
if ($("hp-oo-sheets-a")) {
|
||||||
@@ -1618,16 +1612,12 @@
|
|||||||
if (!matchesOoMoneyFilter(state.legA) || !matchesOoMoneyFilter(state.legB)) {
|
if (!matchesOoMoneyFilter(state.legA) || !matchesOoMoneyFilter(state.legB)) {
|
||||||
throw new Error("期期两腿须为平值或虚值,不可选实值");
|
throw new Error("期期两腿须为平值或虚值,不可选实值");
|
||||||
}
|
}
|
||||||
const up = Number(($("hp-target-up") && $("hp-target-up").value) || 0);
|
const rr = numInput("hp-oo-rr", 2);
|
||||||
const down = Number(($("hp-target-down") && $("hp-target-down").value) || 0);
|
if (!(rr > 0)) throw new Error("请填写盈亏比(相对权利金,默认2)");
|
||||||
if (!up || !down) throw new Error("请填写上破与下破目标价");
|
|
||||||
if (up <= down) throw new Error("上破目标价必须大于下破目标价");
|
|
||||||
body = {
|
body = {
|
||||||
plan_type: "options_options",
|
plan_type: "options_options",
|
||||||
target_price_up: up,
|
oo_profit_rr: rr,
|
||||||
target_price_down: down,
|
index_px: indexPx() || 0,
|
||||||
target_price: up,
|
|
||||||
index_px: indexPx() || (up + down) / 2,
|
|
||||||
leg_a: legPayload(state.legA, ooSheets("hp-oo-sheets-a")),
|
leg_a: legPayload(state.legA, ooSheets("hp-oo-sheets-a")),
|
||||||
leg_b: legPayload(state.legB, ooSheets("hp-oo-sheets-b")),
|
leg_b: legPayload(state.legB, ooSheets("hp-oo-sheets-b")),
|
||||||
};
|
};
|
||||||
@@ -1719,28 +1709,44 @@
|
|||||||
fmt(s.premium_paid) +
|
fmt(s.premium_paid) +
|
||||||
(s.hedge_ratio_at_sl != null ? " · 止损对冲率 " + fmt(s.hedge_ratio_at_sl) + "%" : "");
|
(s.hedge_ratio_at_sl != null ? " · 止损对冲率 " + fmt(s.hedge_ratio_at_sl) + "%" : "");
|
||||||
} else {
|
} else {
|
||||||
const upTot = s.at_target_up_total != null ? s.at_target_up_total : s.at_target_total;
|
const rr = s.oo_profit_rr != null ? s.oo_profit_rr : s.rr_target;
|
||||||
const dnTot = s.at_target_down_total;
|
const tgt = s.target_profit != null ? s.target_profit : s.at_target_total;
|
||||||
let rrLine = "";
|
if (rr != null) {
|
||||||
if (s.rr_at_up != null || s.rr_at_down != null) {
|
summary.innerHTML =
|
||||||
rrLine =
|
"盈亏比 ×" +
|
||||||
" · 盈亏比 上破 " +
|
fmt(rr, 2) +
|
||||||
fmtRr(s.rr_at_up) +
|
" · 目标盈利 " +
|
||||||
(dnTot != null ? " / 下破 " + fmtRr(s.rr_at_down) : "") +
|
fmtPnlHtml(tgt) +
|
||||||
'<span class="muted">(亏=全额保费 ' +
|
" · 到期现价 " +
|
||||||
fmt(s.rr_risk_premium != null ? s.rr_risk_premium : s.premium_paid) +
|
fmtPnlHtml(s.expiry_flat_total) +
|
||||||
")</span>";
|
" · 保费 " +
|
||||||
|
fmt(s.premium_paid) +
|
||||||
|
'<span class="muted">(达标全平;不达标等到期)</span>' +
|
||||||
|
(s.expiry_is_loss ? " · 到期现价情景为亏" : "");
|
||||||
|
} 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;
|
||||||
|
let rrLine = "";
|
||||||
|
if (s.rr_at_up != null || s.rr_at_down != null) {
|
||||||
|
rrLine =
|
||||||
|
" · 盈亏比 上破 " +
|
||||||
|
fmtRr(s.rr_at_up) +
|
||||||
|
(dnTot != null ? " / 下破 " + fmtRr(s.rr_at_down) : "") +
|
||||||
|
'<span class="muted">(亏=全额保费 ' +
|
||||||
|
fmt(s.rr_risk_premium != null ? s.rr_risk_premium : s.premium_paid) +
|
||||||
|
")</span>";
|
||||||
|
}
|
||||||
|
summary.innerHTML =
|
||||||
|
"上破 " +
|
||||||
|
fmtPnlHtml(upTot) +
|
||||||
|
(dnTot != null ? " · 下破 " + fmtPnlHtml(dnTot) : "") +
|
||||||
|
" · 到期现价 " +
|
||||||
|
fmtPnlHtml(s.expiry_flat_total) +
|
||||||
|
" · 保费 " +
|
||||||
|
fmt(s.premium_paid) +
|
||||||
|
rrLine +
|
||||||
|
(s.expiry_is_loss ? " · 到期无盈利(记总亏损)" : "");
|
||||||
}
|
}
|
||||||
summary.innerHTML =
|
|
||||||
"上破 " +
|
|
||||||
fmtPnlHtml(upTot) +
|
|
||||||
(dnTot != null ? " · 下破 " + fmtPnlHtml(dnTot) : "") +
|
|
||||||
" · 到期现价 " +
|
|
||||||
fmtPnlHtml(s.expiry_flat_total) +
|
|
||||||
" · 保费 " +
|
|
||||||
fmt(s.premium_paid) +
|
|
||||||
rrLine +
|
|
||||||
(s.expiry_is_loss ? " · 到期无盈利(记总亏损)" : "");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!tbody) return;
|
if (!tbody) return;
|
||||||
@@ -2057,8 +2063,7 @@
|
|||||||
"hp-tp",
|
"hp-tp",
|
||||||
"hp-sl",
|
"hp-sl",
|
||||||
"hp-sheets",
|
"hp-sheets",
|
||||||
"hp-target-up",
|
"hp-oo-rr",
|
||||||
"hp-target-down",
|
|
||||||
]);
|
]);
|
||||||
if ($("hp-preview-btn"))
|
if ($("hp-preview-btn"))
|
||||||
$("hp-preview-btn").addEventListener("click", function () {
|
$("hp-preview-btn").addEventListener("click", function () {
|
||||||
@@ -2171,6 +2176,9 @@
|
|||||||
if (p.plan_type === "perp_options") {
|
if (p.plan_type === "perp_options") {
|
||||||
return "止盈 " + fmt(p.tp) + " · 止损 " + fmt(p.sl);
|
return "止盈 " + fmt(p.tp) + " · 止损 " + fmt(p.sl);
|
||||||
}
|
}
|
||||||
|
if (p.oo_profit_rr != null && Number(p.oo_profit_rr) > 0) {
|
||||||
|
return "盈亏比 ×" + fmt(p.oo_profit_rr, 2) + "(达标全平)";
|
||||||
|
}
|
||||||
return "上破 " + fmt(p.target_price_up || p.target_price) + " · 下破 " + fmt(p.target_price_down || p.target_price);
|
return "上破 " + fmt(p.target_price_up || p.target_price) + " · 下破 " + fmt(p.target_price_down || p.target_price);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2330,6 +2338,8 @@
|
|||||||
target_win_leg: "期期平盈利腿",
|
target_win_leg: "期期平盈利腿",
|
||||||
target_up_win_leg: "期期上破·平盈利腿",
|
target_up_win_leg: "期期上破·平盈利腿",
|
||||||
target_down_win_leg: "期期下破·平盈利腿",
|
target_down_win_leg: "期期下破·平盈利腿",
|
||||||
|
oo_rr_target: "期期盈亏比达标",
|
||||||
|
oo_rr_closing: "期期盈亏比平仓中",
|
||||||
oo_rest_closing: "期期全平·清残腿中",
|
oo_rest_closing: "期期全平·清残腿中",
|
||||||
oo_rest_closed: "期期全平·两腿已平",
|
oo_rest_closed: "期期全平·两腿已平",
|
||||||
orphaned_after_tp: "止盈后持有至到期",
|
orphaned_after_tp: "止盈后持有至到期",
|
||||||
@@ -2404,6 +2414,11 @@
|
|||||||
"x · 张数 " +
|
"x · 张数 " +
|
||||||
fmt(p.perp_size, 4) +
|
fmt(p.perp_size, 4) +
|
||||||
"</div>";
|
"</div>";
|
||||||
|
} else if (p.oo_profit_rr != null && Number(p.oo_profit_rr) > 0) {
|
||||||
|
html +=
|
||||||
|
"<div><span class=\"muted\">盈亏比</span> ×" +
|
||||||
|
fmt(p.oo_profit_rr, 2) +
|
||||||
|
"(浮盈达标全平;不达标等到期)</div>";
|
||||||
} else {
|
} else {
|
||||||
html +=
|
html +=
|
||||||
"<div><span class=\"muted\">目标价</span> 上破 " +
|
"<div><span class=\"muted\">目标价</span> 上破 " +
|
||||||
@@ -2626,17 +2641,13 @@
|
|||||||
if (!matchesOoMoneyFilter(state.legA) || !matchesOoMoneyFilter(state.legB)) {
|
if (!matchesOoMoneyFilter(state.legA) || !matchesOoMoneyFilter(state.legB)) {
|
||||||
throw new Error("期期两腿须为平值或虚值,不可选实值");
|
throw new Error("期期两腿须为平值或虚值,不可选实值");
|
||||||
}
|
}
|
||||||
const up = Number(($("hp-target-up") && $("hp-target-up").value) || 0);
|
const rr = numInput("hp-oo-rr", 2);
|
||||||
const down = Number(($("hp-target-down") && $("hp-target-down").value) || 0);
|
if (!(rr > 0)) throw new Error("请填写盈亏比(相对权利金,默认2)");
|
||||||
if (!up || !down) throw new Error("请填写上破与下破目标价");
|
|
||||||
if (up <= down) throw new Error("上破目标价必须大于下破目标价");
|
|
||||||
body = {
|
body = {
|
||||||
plan_type: "options_options",
|
plan_type: "options_options",
|
||||||
underlying: state.underlying,
|
underlying: state.underlying,
|
||||||
target_price_up: up,
|
oo_profit_rr: rr,
|
||||||
target_price_down: down,
|
index_px: indexPx() || 0,
|
||||||
target_price: up,
|
|
||||||
index_px: indexPx() || (up + down) / 2,
|
|
||||||
oo_close_mode: state.ooCloseModeEnabled ? state.ooCloseMode : "hold_expiry",
|
oo_close_mode: state.ooCloseModeEnabled ? state.ooCloseMode : "hold_expiry",
|
||||||
oo_sheets_mode: state.ooSheetsMode || "same_sheets",
|
oo_sheets_mode: state.ooSheetsMode || "same_sheets",
|
||||||
leg_a: legPayload(state.legA, ooSheets("hp-oo-sheets-a")),
|
leg_a: legPayload(state.legA, ooSheets("hp-oo-sheets-a")),
|
||||||
|
|||||||
@@ -76,6 +76,8 @@
|
|||||||
target_win_leg: "期期平盈利腿",
|
target_win_leg: "期期平盈利腿",
|
||||||
target_up_win_leg: "期期上破·平盈利腿",
|
target_up_win_leg: "期期上破·平盈利腿",
|
||||||
target_down_win_leg: "期期下破·平盈利腿",
|
target_down_win_leg: "期期下破·平盈利腿",
|
||||||
|
oo_rr_target: "期期盈亏比达标",
|
||||||
|
oo_rr_closing: "期期盈亏比平仓中",
|
||||||
oo_rest_closing: "期期全平·清残腿中",
|
oo_rest_closing: "期期全平·清残腿中",
|
||||||
oo_rest_closed: "期期全平·两腿已平",
|
oo_rest_closed: "期期全平·两腿已平",
|
||||||
orphaned_after_tp: "止盈后持有至到期",
|
orphaned_after_tp: "止盈后持有至到期",
|
||||||
|
|||||||
@@ -444,6 +444,7 @@ def _hedge_ratio(opt_pnl: float, perp_pnl: float) -> Optional[float]:
|
|||||||
|
|
||||||
def build_options_options_preview(
|
def build_options_options_preview(
|
||||||
*,
|
*,
|
||||||
|
profit_rr: float | None = None,
|
||||||
target_price: float | None = None,
|
target_price: float | None = None,
|
||||||
target_price_up: float | None = None,
|
target_price_up: float | None = None,
|
||||||
target_price_down: float | None = None,
|
target_price_down: float | None = None,
|
||||||
@@ -451,7 +452,11 @@ def build_options_options_preview(
|
|||||||
leg_a: dict[str, Any],
|
leg_a: dict[str, Any],
|
||||||
leg_b: dict[str, Any],
|
leg_b: dict[str, Any],
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""期期情景:上破/下破目标价 / 到期现价 / 最大保费损耗."""
|
"""期期情景:盈亏比达标 / 到期现价 / 最大保费损耗.
|
||||||
|
|
||||||
|
profit_rr=2 表示目标盈利=2×权利金;中途不达标则等到期.
|
||||||
|
仍接受旧上破/下破参数仅作兼容测算.
|
||||||
|
"""
|
||||||
|
|
||||||
def _leg_pnl(leg: dict[str, Any], spot: float) -> float:
|
def _leg_pnl(leg: dict[str, Any], spot: float) -> float:
|
||||||
return option_expiry_pnl(
|
return option_expiry_pnl(
|
||||||
@@ -463,15 +468,73 @@ def build_options_options_preview(
|
|||||||
premium_paid=float(leg.get("premium_paid") or 0),
|
premium_paid=float(leg.get("premium_paid") or 0),
|
||||||
)
|
)
|
||||||
|
|
||||||
# 兼容旧单目标:若未传上下目标则用 target_price 填两边
|
prem = float(leg_a.get("premium_paid") or 0) + float(leg_b.get("premium_paid") or 0)
|
||||||
|
a_flat = _leg_pnl(leg_a, index_px)
|
||||||
|
b_flat = _leg_pnl(leg_b, index_px)
|
||||||
|
flat_total = a_flat + b_flat
|
||||||
|
|
||||||
|
rr = None
|
||||||
|
if profit_rr not in (None, ""):
|
||||||
|
try:
|
||||||
|
rr = float(profit_rr)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
rr = None
|
||||||
|
if rr is not None and rr > 0:
|
||||||
|
target_pnl = rr * prem
|
||||||
|
return {
|
||||||
|
"plan_type": "options_options",
|
||||||
|
"premium_paid": round(prem, 6),
|
||||||
|
"oo_profit_rr": round(rr, 4),
|
||||||
|
"target_profit": round(target_pnl, 4),
|
||||||
|
"scenarios": [
|
||||||
|
{
|
||||||
|
"id": "rr_target",
|
||||||
|
"label": f"盈亏比×{rr:g}",
|
||||||
|
"spot": None,
|
||||||
|
"leg_a_pnl": None,
|
||||||
|
"leg_b_pnl": None,
|
||||||
|
"total": round(target_pnl, 4),
|
||||||
|
"note": f"两腿合计浮盈≥{rr:g}×权利金({round(prem, 4)})时全平;不达标等到期",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "中途未达盈亏比则持有至到期结算",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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),
|
||||||
|
"total": round(-prem, 4),
|
||||||
|
"note": "双腿权利金全部损失",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"summary": {
|
||||||
|
"oo_profit_rr": round(rr, 4),
|
||||||
|
"target_profit": round(target_pnl, 4),
|
||||||
|
"at_target_total": round(target_pnl, 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_target": round(rr, 4),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# 兼容旧上破/下破测算
|
||||||
up = target_price_up if target_price_up is not None else 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
|
down = target_price_down if target_price_down is not None else target_price
|
||||||
if up is None or down is None:
|
if up is None or down is None:
|
||||||
raise ValueError("缺少上破/下破目标价")
|
raise ValueError("请填写盈亏比(相对权利金,默认2)")
|
||||||
up_f = float(up)
|
up_f = float(up)
|
||||||
down_f = float(down)
|
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)
|
a_up = _leg_pnl(leg_a, up_f)
|
||||||
b_up = _leg_pnl(leg_b, up_f)
|
b_up = _leg_pnl(leg_b, up_f)
|
||||||
at_up = a_up + b_up
|
at_up = a_up + b_up
|
||||||
@@ -482,15 +545,10 @@ def build_options_options_preview(
|
|||||||
at_dn = a_dn + b_dn
|
at_dn = a_dn + b_dn
|
||||||
win_dn = "a" if a_dn >= b_dn else "b"
|
win_dn = "a" if a_dn >= b_dn else "b"
|
||||||
|
|
||||||
a_flat = _leg_pnl(leg_a, index_px)
|
|
||||||
b_flat = _leg_pnl(leg_b, index_px)
|
|
||||||
flat_total = a_flat + b_flat
|
|
||||||
expiry_loss = flat_total if flat_total <= 0 else flat_total
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"plan_type": "options_options",
|
"plan_type": "options_options",
|
||||||
"premium_paid": round(prem, 6),
|
"premium_paid": round(prem, 6),
|
||||||
"target_price": up_f, # 兼容旧字段,取上破
|
"target_price": up_f,
|
||||||
"target_price_up": up_f,
|
"target_price_up": up_f,
|
||||||
"target_price_down": down_f,
|
"target_price_down": down_f,
|
||||||
"winner_at_up": win_up,
|
"winner_at_up": win_up,
|
||||||
@@ -538,10 +596,9 @@ def build_options_options_preview(
|
|||||||
"at_target_up_total": round(at_up, 4),
|
"at_target_up_total": round(at_up, 4),
|
||||||
"at_target_down_total": round(at_dn, 4),
|
"at_target_down_total": round(at_dn, 4),
|
||||||
"at_target_total": round(at_up, 4),
|
"at_target_total": round(at_up, 4),
|
||||||
"expiry_flat_total": round(expiry_loss, 4),
|
"expiry_flat_total": round(flat_total, 4),
|
||||||
"premium_paid": round(prem, 6),
|
"premium_paid": round(prem, 6),
|
||||||
"expiry_is_loss": flat_total <= 0,
|
"expiry_is_loss": flat_total <= 0,
|
||||||
# 盈亏比:盈利/全亏保费(风险=权利金全损)
|
|
||||||
"rr_risk_premium": round(prem, 6),
|
"rr_risk_premium": round(prem, 6),
|
||||||
"rr_at_up": round(at_up / prem, 4) if prem > 0 else None,
|
"rr_at_up": round(at_up / prem, 4) if prem > 0 else None,
|
||||||
"rr_at_down": round(at_dn / prem, 4) if prem > 0 else None,
|
"rr_at_down": round(at_dn / prem, 4) if prem > 0 else None,
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ 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_up", "REAL")
|
||||||
_ensure_column(conn, "hedge_plans", "target_price_down", "REAL")
|
_ensure_column(conn, "hedge_plans", "target_price_down", "REAL")
|
||||||
|
# 期期:目标盈亏比=目标盈利/权利金(如 2=盈利 2 倍权利金);不达标则等到期
|
||||||
|
_ensure_column(conn, "hedge_plans", "oo_profit_rr", "REAL")
|
||||||
# close_all=盈利腿平后清残腿;hold_expiry=残腿持有至到期(现状)
|
# close_all=盈利腿平后清残腿;hold_expiry=残腿持有至到期(现状)
|
||||||
_ensure_column(conn, "hedge_plans", "oo_close_mode", "TEXT")
|
_ensure_column(conn, "hedge_plans", "oo_close_mode", "TEXT")
|
||||||
# 永期「以期权为主」
|
# 永期「以期权为主」
|
||||||
@@ -272,7 +274,7 @@ def active_options_targets_by_inst(conn: sqlite3.Connection) -> dict[str, dict[s
|
|||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
"""
|
"""
|
||||||
SELECT p.id AS plan_id, p.underlying, p.target_price_up, p.target_price_down,
|
SELECT p.id AS plan_id, p.underlying, p.target_price_up, p.target_price_down,
|
||||||
l.inst_id, l.opt_type
|
p.oo_profit_rr, l.inst_id, l.opt_type
|
||||||
FROM hedge_plans p
|
FROM hedge_plans p
|
||||||
JOIN hedge_plan_legs l ON l.plan_id = p.id
|
JOIN hedge_plan_legs l ON l.plan_id = p.id
|
||||||
WHERE p.plan_type = 'options_options'
|
WHERE p.plan_type = 'options_options'
|
||||||
@@ -287,10 +289,36 @@ def active_options_targets_by_inst(conn: sqlite3.Connection) -> dict[str, dict[s
|
|||||||
for raw in rows:
|
for raw in rows:
|
||||||
row = dict(raw)
|
row = dict(raw)
|
||||||
inst_id = str(row.get("inst_id") or "")
|
inst_id = str(row.get("inst_id") or "")
|
||||||
|
if not inst_id or inst_id in out:
|
||||||
|
continue
|
||||||
opt_type = str(row.get("opt_type") or "").upper()
|
opt_type = str(row.get("opt_type") or "").upper()
|
||||||
|
rr = _sf(row.get("oo_profit_rr"))
|
||||||
target = row.get("target_price_up") if opt_type == "C" else row.get("target_price_down")
|
target = row.get("target_price_up") if opt_type == "C" else row.get("target_price_down")
|
||||||
target_f = _sf(target)
|
target_f = _sf(target)
|
||||||
if not inst_id or target_f is None or target_f <= 0 or inst_id in out:
|
# 盈亏比模式无指数目标价;旧上破/下破计划仍透出 target_index 只读展示
|
||||||
|
if rr is not None and rr > 0:
|
||||||
|
out[inst_id] = {
|
||||||
|
"plan_id": int(row["plan_id"]),
|
||||||
|
"inst_id": inst_id,
|
||||||
|
"underlying": row.get("underlying"),
|
||||||
|
"opt_type": opt_type,
|
||||||
|
"target_index": None,
|
||||||
|
"oo_profit_rr": rr,
|
||||||
|
"plan_type": "options_options",
|
||||||
|
"managed_by": "hedge_plan",
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
if target_f is None or target_f <= 0:
|
||||||
|
# 无目标价也标记托管,避免期权页误拆组
|
||||||
|
out[inst_id] = {
|
||||||
|
"plan_id": int(row["plan_id"]),
|
||||||
|
"inst_id": inst_id,
|
||||||
|
"underlying": row.get("underlying"),
|
||||||
|
"opt_type": opt_type,
|
||||||
|
"target_index": None,
|
||||||
|
"plan_type": "options_options",
|
||||||
|
"managed_by": "hedge_plan",
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
out[inst_id] = {
|
out[inst_id] = {
|
||||||
"plan_id": int(row["plan_id"]),
|
"plan_id": int(row["plan_id"]),
|
||||||
|
|||||||
@@ -999,6 +999,8 @@ def _tick_oo_close_rest(
|
|||||||
"target_up_win_leg",
|
"target_up_win_leg",
|
||||||
"target_down_win_leg",
|
"target_down_win_leg",
|
||||||
"oo_rest_closing",
|
"oo_rest_closing",
|
||||||
|
"oo_rr_closing",
|
||||||
|
"oo_rr_target",
|
||||||
"",
|
"",
|
||||||
)
|
)
|
||||||
if reason0 not in allowed_reasons and not (
|
if reason0 not in allowed_reasons and not (
|
||||||
@@ -1049,7 +1051,113 @@ def _tick_oo_close_rest(
|
|||||||
def _tick_oo_target(
|
def _tick_oo_target(
|
||||||
cfg: dict[str, Any], conn: Any, plan: dict[str, Any], legs: list[dict[str, Any]]
|
cfg: dict[str, Any], conn: Any, plan: dict[str, Any], legs: list[dict[str, Any]]
|
||||||
) -> Optional[dict[str, Any]]:
|
) -> Optional[dict[str, Any]]:
|
||||||
"""期期:触及上破或下破目标价时平盈利腿;按平仓模式处理另一腿."""
|
"""期期止盈:优先盈亏比(浮盈≥rr×权利金则两腿全平);否则兼容旧上破/下破."""
|
||||||
|
rr = _sf(plan.get("oo_profit_rr"))
|
||||||
|
if rr is not None and rr > 0:
|
||||||
|
return _tick_oo_rr_target(cfg, conn, plan, legs, rr=float(rr))
|
||||||
|
return _tick_oo_price_target(cfg, conn, plan, legs)
|
||||||
|
|
||||||
|
|
||||||
|
def _tick_oo_rr_target(
|
||||||
|
cfg: dict[str, Any],
|
||||||
|
conn: Any,
|
||||||
|
plan: dict[str, Any],
|
||||||
|
legs: list[dict[str, Any]],
|
||||||
|
*,
|
||||||
|
rr: float,
|
||||||
|
) -> Optional[dict[str, Any]]:
|
||||||
|
"""浮盈(买一回收−权利金)≥盈亏比×总权利金 → 两腿全平;不达标则等到期."""
|
||||||
|
open_legs = _oo_option_legs(legs, statuses=("open",))
|
||||||
|
if len(open_legs) < 1:
|
||||||
|
return None
|
||||||
|
premium = float(plan.get("premium_total") or 0)
|
||||||
|
if premium <= 0:
|
||||||
|
premium = sum(float(x.get("premium") or 0) for x in open_legs)
|
||||||
|
if premium <= 0:
|
||||||
|
return None
|
||||||
|
need = float(rr) * premium
|
||||||
|
quote_fn = cfg.get("quote_option_contract")
|
||||||
|
ex = cfg.get("exchange_options")
|
||||||
|
if not callable(quote_fn) or ex is None:
|
||||||
|
return None
|
||||||
|
idx = _index_px(cfg, str(plan.get("underlying") or "ETH"))
|
||||||
|
total_pnl = 0.0
|
||||||
|
missing_bid = 0
|
||||||
|
for leg in open_legs:
|
||||||
|
inst = str(leg.get("inst_id") or "")
|
||||||
|
bid = None
|
||||||
|
try:
|
||||||
|
q = quote_fn(ex, inst) if inst else {}
|
||||||
|
if isinstance(q, dict) and q.get("ok"):
|
||||||
|
bid = _sf(q.get("bid"))
|
||||||
|
except Exception:
|
||||||
|
bid = None
|
||||||
|
if bid is None or float(bid) <= 0:
|
||||||
|
missing_bid += 1
|
||||||
|
# 无买一时用内在价值兜底,避免短暂无盘口卡住;两腿都无买一则本轮跳过
|
||||||
|
total_pnl += _estimate_leg_close_pnl(leg, idx, None)
|
||||||
|
else:
|
||||||
|
total_pnl += _estimate_leg_close_pnl(leg, idx, float(bid))
|
||||||
|
if missing_bid >= len(open_legs):
|
||||||
|
return None
|
||||||
|
if total_pnl + 1e-9 < need:
|
||||||
|
return None
|
||||||
|
|
||||||
|
acted = 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)
|
||||||
|
)
|
||||||
|
if not close_r.get("ok"):
|
||||||
|
notify_hedge(
|
||||||
|
cfg,
|
||||||
|
build_hedge_alert_message(
|
||||||
|
title="期期盈亏比达标·平仓失败(将重试)",
|
||||||
|
plan_id=plan.get("id"),
|
||||||
|
detail=(
|
||||||
|
f"目标 {rr:g}×权利金={need:.4f};估算浮盈 {total_pnl:.4f}; "
|
||||||
|
f"{close_r.get('msg') or close_r}"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
update_plan(conn, int(plan["id"]), close_reason="oo_rr_closing")
|
||||||
|
return {
|
||||||
|
"plan_id": plan["id"],
|
||||||
|
"msg": "盈亏比达标但平仓失败",
|
||||||
|
"close": close_r,
|
||||||
|
"retry": True,
|
||||||
|
"rr": rr,
|
||||||
|
"need": need,
|
||||||
|
"mtm": total_pnl,
|
||||||
|
}
|
||||||
|
bid = _sf(close_r.get("bid"))
|
||||||
|
est = _estimate_leg_close_pnl(leg, idx, bid)
|
||||||
|
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=?",
|
||||||
|
("closed", "oo_rr_target", _now(), round(pnl, 4), leg["id"]),
|
||||||
|
)
|
||||||
|
acted = True
|
||||||
|
|
||||||
|
if not acted:
|
||||||
|
return None
|
||||||
|
legs2 = get_plan_legs(conn, int(plan["id"]))
|
||||||
|
still_open = _oo_option_legs(legs2, statuses=("open", "hold_to_expiry"))
|
||||||
|
if still_open:
|
||||||
|
update_plan(conn, int(plan["id"]), close_reason="oo_rr_closing")
|
||||||
|
return {
|
||||||
|
"plan_id": plan["id"],
|
||||||
|
"msg": "盈亏比达标·部分已平,继续重试",
|
||||||
|
"remaining": len(still_open),
|
||||||
|
"rr": rr,
|
||||||
|
}
|
||||||
|
return _finalize_oo_all_closed(cfg, conn, plan, legs2, reason="oo_rr_target")
|
||||||
|
|
||||||
|
|
||||||
|
def _tick_oo_price_target(
|
||||||
|
cfg: dict[str, Any], conn: Any, plan: dict[str, Any], legs: list[dict[str, Any]]
|
||||||
|
) -> Optional[dict[str, Any]]:
|
||||||
|
"""旧逻辑:触及上破或下破目标价时平盈利腿;按平仓模式处理另一腿."""
|
||||||
idx = _index_px(cfg, str(plan.get("underlying") or "ETH"))
|
idx = _index_px(cfg, str(plan.get("underlying") or "ETH"))
|
||||||
if idx is None:
|
if idx is None:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -46,13 +46,22 @@ def build_hedge_start_message(plan: dict[str, Any], *, legs: Optional[list[dict[
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
lines.extend(
|
rr = plan.get("oo_profit_rr")
|
||||||
[
|
if rr not in (None, ""):
|
||||||
f"🎯 上破:{_fmt(plan.get('target_price_up') or plan.get('target_price'))}"
|
lines.extend(
|
||||||
f"|下破:{_fmt(plan.get('target_price_down') or plan.get('target_price'))}",
|
[
|
||||||
f"💎 期权保费合计:{_fmt(plan.get('premium_total'), 4)} USDC",
|
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:
|
if legs:
|
||||||
for leg in legs:
|
for leg in legs:
|
||||||
role = leg.get("leg_role") or ""
|
role = leg.get("leg_role") or ""
|
||||||
@@ -81,6 +90,8 @@ def build_hedge_end_message(plan: dict[str, Any]) -> str:
|
|||||||
"target_win_leg": "期期已平盈利腿(中间态)",
|
"target_win_leg": "期期已平盈利腿(中间态)",
|
||||||
"target_up_win_leg": "期期上破·已平盈利腿",
|
"target_up_win_leg": "期期上破·已平盈利腿",
|
||||||
"target_down_win_leg": "期期下破·已平盈利腿",
|
"target_down_win_leg": "期期下破·已平盈利腿",
|
||||||
|
"oo_rr_target": "期期盈亏比达标·两腿已平",
|
||||||
|
"oo_rr_closing": "期期盈亏比达标·平仓中",
|
||||||
"oo_rest_closing": "期期全平·清残腿中",
|
"oo_rest_closing": "期期全平·清残腿中",
|
||||||
"oo_rest_closed": "期期全平·两腿已平",
|
"oo_rest_closed": "期期全平·两腿已平",
|
||||||
"oo_expiry_loss": "期期到期无盈利·总亏损",
|
"oo_expiry_loss": "期期到期无盈利·总亏损",
|
||||||
@@ -153,7 +164,18 @@ def notify_plan_end(cfg: dict[str, Any], conn: Any, plan: dict[str, Any]) -> boo
|
|||||||
"target_up_win_leg",
|
"target_up_win_leg",
|
||||||
"target_down_win_leg",
|
"target_down_win_leg",
|
||||||
"oo_rest_closing",
|
"oo_rest_closing",
|
||||||
|
"oo_rr_closing",
|
||||||
) and (plan.get("status") or "") != "closed":
|
) and (plan.get("status") or "") != "closed":
|
||||||
|
if "oo_rr" in str(plan.get("close_reason") or ""):
|
||||||
|
notify_hedge(
|
||||||
|
cfg,
|
||||||
|
build_hedge_alert_message(
|
||||||
|
title="期期盈亏比达标·平仓进行中",
|
||||||
|
plan_id=plan.get("id"),
|
||||||
|
detail=f"盈亏比 {_fmt(plan.get('oo_profit_rr'))}×权利金",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return True
|
||||||
side = "上破" if "up" in str(plan.get("close_reason")) else (
|
side = "上破" if "up" in str(plan.get("close_reason")) else (
|
||||||
"下破" if "down" in str(plan.get("close_reason")) else "目标价"
|
"下破" if "down" in str(plan.get("close_reason")) else "目标价"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1146,20 +1146,31 @@ def validate_start_body(plan_type: str, body: dict[str, Any]) -> Optional[str]:
|
|||||||
b = body.get("leg_b") or {}
|
b = body.get("leg_b") or {}
|
||||||
if not a.get("inst_id") or not b.get("inst_id"):
|
if not a.get("inst_id") or not b.get("inst_id"):
|
||||||
return "请选用两条期权腿"
|
return "请选用两条期权腿"
|
||||||
up = body.get("target_price_up")
|
rr_raw = body.get("oo_profit_rr")
|
||||||
down = body.get("target_price_down")
|
if rr_raw in (None, ""):
|
||||||
legacy = body.get("target_price")
|
rr_raw = body.get("profit_rr")
|
||||||
if up in (None, "") and legacy not in (None, ""):
|
if rr_raw not in (None, ""):
|
||||||
up = legacy
|
try:
|
||||||
if down in (None, "") and legacy not in (None, ""):
|
rr = float(rr_raw)
|
||||||
down = legacy
|
except (TypeError, ValueError):
|
||||||
if up in (None, "") or down in (None, ""):
|
return "盈亏比无效"
|
||||||
return "请填写上破与下破目标价"
|
if rr <= 0:
|
||||||
try:
|
return "盈亏比须大于 0"
|
||||||
if float(up) <= float(down):
|
else:
|
||||||
return "上破目标价必须大于下破目标价"
|
up = body.get("target_price_up")
|
||||||
except (TypeError, ValueError):
|
down = body.get("target_price_down")
|
||||||
return "目标价无效"
|
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 "请填写盈亏比(相对权利金,默认2)"
|
||||||
|
try:
|
||||||
|
if float(up) <= float(down):
|
||||||
|
return "上破目标价必须大于下破目标价"
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return "目标价无效"
|
||||||
from lib.hedge_plan.hedge_plan_moneyness_lib import (
|
from lib.hedge_plan.hedge_plan_moneyness_lib import (
|
||||||
parse_strike_from_inst,
|
parse_strike_from_inst,
|
||||||
validate_oo_legs_moneyness,
|
validate_oo_legs_moneyness,
|
||||||
@@ -1179,11 +1190,6 @@ def validate_start_body(plan_type: str, body: dict[str, Any]) -> Optional[str]:
|
|||||||
return {"opt_type": opt_type, "strike": strike}
|
return {"opt_type": opt_type, "strike": strike}
|
||||||
|
|
||||||
index_px = body.get("index_px")
|
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(
|
money_err = validate_oo_legs_moneyness(
|
||||||
_leg_for_money(a),
|
_leg_for_money(a),
|
||||||
_leg_for_money(b),
|
_leg_for_money(b),
|
||||||
|
|||||||
@@ -537,27 +537,25 @@ 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) + (
|
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
|
float(b.get("premium") or 0) if b_ok else 0.0
|
||||||
)
|
)
|
||||||
|
rr_raw = body.get("oo_profit_rr")
|
||||||
|
if rr_raw in (None, ""):
|
||||||
|
rr_raw = body.get("profit_rr")
|
||||||
|
try:
|
||||||
|
oo_rr = float(rr_raw) if rr_raw not in (None, "") else 2.0
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
oo_rr = 2.0
|
||||||
|
if oo_rr <= 0:
|
||||||
|
oo_rr = 2.0
|
||||||
plan_id = insert_plan(
|
plan_id = insert_plan(
|
||||||
conn,
|
conn,
|
||||||
{
|
{
|
||||||
"plan_type": "options_options",
|
"plan_type": "options_options",
|
||||||
"status": "partial" if is_partial else "active",
|
"status": "partial" if is_partial else "active",
|
||||||
"underlying": str(body.get("underlying") or "ETH").upper(),
|
"underlying": str(body.get("underlying") or "ETH").upper(),
|
||||||
"target_price": float(
|
"target_price": None,
|
||||||
body.get("target_price_up")
|
"target_price_up": None,
|
||||||
or body.get("target_price")
|
"target_price_down": None,
|
||||||
or 0
|
"oo_profit_rr": oo_rr,
|
||||||
),
|
|
||||||
"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
|
|
||||||
),
|
|
||||||
"sizing_mode_at_open": load_position_sizing_mode(),
|
"sizing_mode_at_open": load_position_sizing_mode(),
|
||||||
"premium_total": premium,
|
"premium_total": premium,
|
||||||
"oo_close_mode": _normalize_oo_close_mode(body.get("oo_close_mode")),
|
"oo_close_mode": _normalize_oo_close_mode(body.get("oo_close_mode")),
|
||||||
@@ -1238,20 +1236,24 @@ def _preview_po(body: dict[str, Any]) -> dict[str, Any]:
|
|||||||
def _preview_oo(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
|
from lib.hedge_plan.hedge_plan_moneyness_lib import validate_oo_legs_moneyness
|
||||||
|
|
||||||
up = body.get("target_price_up")
|
rr_raw = body.get("oo_profit_rr")
|
||||||
down = body.get("target_price_down")
|
if rr_raw in (None, ""):
|
||||||
legacy = body.get("target_price")
|
rr_raw = body.get("profit_rr")
|
||||||
if up in (None, "") and legacy not in (None, ""):
|
rr = None
|
||||||
up = legacy
|
if rr_raw not in (None, ""):
|
||||||
if down in (None, "") and legacy not in (None, ""):
|
try:
|
||||||
down = legacy
|
rr = float(rr_raw)
|
||||||
if up in (None, "") or down in (None, ""):
|
except (TypeError, ValueError) as e:
|
||||||
raise ValueError("请填写上破与下破目标价")
|
raise ValueError("盈亏比无效") from e
|
||||||
up_f = float(up)
|
if rr <= 0:
|
||||||
down_f = float(down)
|
raise ValueError("盈亏比须大于 0")
|
||||||
if up_f <= down_f:
|
|
||||||
raise ValueError("上破目标价必须大于下破目标价")
|
index_px = body.get("index_px")
|
||||||
index_px = float(body.get("index_px") or ((up_f + down_f) / 2))
|
try:
|
||||||
|
index_px_f = float(index_px) if index_px not in (None, "") else 0.0
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
index_px_f = 0.0
|
||||||
|
|
||||||
leg_a = body.get("leg_a") or {}
|
leg_a = body.get("leg_a") or {}
|
||||||
leg_b = body.get("leg_b") or {}
|
leg_b = body.get("leg_b") or {}
|
||||||
for name, leg in (("leg_a", leg_a), ("leg_b", leg_b)):
|
for name, leg in (("leg_a", leg_a), ("leg_b", leg_b)):
|
||||||
@@ -1265,13 +1267,38 @@ def _preview_oo(body: dict[str, Any]) -> dict[str, Any]:
|
|||||||
)
|
)
|
||||||
if leg.get("premium_paid") is None:
|
if leg.get("premium_paid") is None:
|
||||||
raise ValueError(f"缺少 {name} 权利金")
|
raise ValueError(f"缺少 {name} 权利金")
|
||||||
money_err = validate_oo_legs_moneyness(leg_a, leg_b, index_px=index_px)
|
money_err = validate_oo_legs_moneyness(leg_a, leg_b, index_px=index_px_f or None)
|
||||||
if money_err:
|
if money_err:
|
||||||
raise ValueError(money_err)
|
raise ValueError(money_err)
|
||||||
|
|
||||||
|
if rr is not None:
|
||||||
|
return build_options_options_preview(
|
||||||
|
profit_rr=rr,
|
||||||
|
index_px=index_px_f,
|
||||||
|
leg_a=leg_a,
|
||||||
|
leg_b=leg_b,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 兼容旧上破/下破
|
||||||
|
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, ""):
|
||||||
|
raise ValueError("请填写盈亏比(相对权利金,默认2)")
|
||||||
|
up_f = float(up)
|
||||||
|
down_f = float(down)
|
||||||
|
if up_f <= down_f:
|
||||||
|
raise ValueError("上破目标价必须大于下破目标价")
|
||||||
|
if index_px_f <= 0:
|
||||||
|
index_px_f = (up_f + down_f) / 2
|
||||||
return build_options_options_preview(
|
return build_options_options_preview(
|
||||||
target_price_up=up_f,
|
target_price_up=up_f,
|
||||||
target_price_down=down_f,
|
target_price_down=down_f,
|
||||||
index_px=index_px,
|
index_px=index_px_f,
|
||||||
leg_a=leg_a,
|
leg_a=leg_a,
|
||||||
leg_b=leg_b,
|
leg_b=leg_b,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -213,7 +213,7 @@
|
|||||||
<div class="tip-collapse-body rule-tip">
|
<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>:两腿都在<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>:选 Call + Put 后「计算」再「启动」。启动会再拉卖一并按最新价重算张数,IOC 完全成交才算成功;资金不足可在右侧划转。</p>
|
||||||
<p><strong>板块</strong>:左填上破/下破与张数模式(同张数/做多/做空);右 T 型选腿。<strong>两腿仅允许平值或虚值</strong>(禁实值)。「全平」= 盈利腿平后清另一腿;「到期平」= 另一腿持有至到期。</p>
|
<p><strong>板块</strong>:左填<strong>盈亏比</strong>(相对权利金,默认 2=盈利 2 倍权利金)与张数模式(同张数/做多/做空);右 T 型选腿。<strong>两腿仅允许平值或虚值</strong>(禁实值)。中途浮盈达盈亏比→两腿全平;不达标→等到期。「全平/到期平」仅兼容旧上破下破计划残腿处理。</p>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
<div class="form-row hp-uly-row">
|
<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>
|
<button type="button" class="btn-secondary hp-uly-btn-oo" data-uly="BTC">BTC</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row hp-target-row hp-oo-target-row">
|
<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 title="目标盈利 = 盈亏比 × 两腿权利金合计;例 2=赚满 2 倍权利金后全平">盈亏比 <input type="number" step="0.1" min="0.1" id="hp-oo-rr" value="2" placeholder="默认2" 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>
|
|
||||||
<span id="hp-oo-index" class="hp-oo-index" aria-live="polite">指数 —</span>
|
<span id="hp-oo-index" class="hp-oo-index" aria-live="polite">指数 —</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="hp-oo-controls">
|
<div class="hp-oo-controls">
|
||||||
@@ -406,4 +405,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/static/hedge_plan.js?v=46"></script>
|
<script src="/static/hedge_plan.js?v=47"></script>
|
||||||
|
|||||||
@@ -416,4 +416,4 @@
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/options_review.js?v=23"></script>
|
<script src="/static/options_review.js?v=24"></script>
|
||||||
|
|||||||
@@ -102,8 +102,7 @@ class TestHedgePlanCalc(unittest.TestCase):
|
|||||||
a = {"opt_type": "C", "strike": 3300, "sheets": 1, "ct_mult": 0.01, "premium_paid": 5}
|
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}
|
b = {"opt_type": "P", "strike": 3100, "sheets": 1, "ct_mult": 0.01, "premium_paid": 5}
|
||||||
p = build_options_options_preview(
|
p = build_options_options_preview(
|
||||||
target_price_up=3500,
|
profit_rr=2,
|
||||||
target_price_down=3000,
|
|
||||||
index_px=3200,
|
index_px=3200,
|
||||||
leg_a=a,
|
leg_a=a,
|
||||||
leg_b=b,
|
leg_b=b,
|
||||||
@@ -111,11 +110,11 @@ class TestHedgePlanCalc(unittest.TestCase):
|
|||||||
self.assertEqual(p["summary"]["premium_paid"], 10)
|
self.assertEqual(p["summary"]["premium_paid"], 10)
|
||||||
self.assertTrue(p["summary"]["expiry_is_loss"])
|
self.assertTrue(p["summary"]["expiry_is_loss"])
|
||||||
self.assertEqual(p["summary"]["rr_risk_premium"], 10)
|
self.assertEqual(p["summary"]["rr_risk_premium"], 10)
|
||||||
self.assertIsNotNone(p["summary"]["rr_at_up"])
|
self.assertEqual(p["summary"]["oo_profit_rr"], 2)
|
||||||
self.assertAlmostEqual(p["summary"]["rr_at_up"], p["summary"]["at_target_up_total"] / 10, places=4)
|
self.assertAlmostEqual(p["summary"]["target_profit"], 20.0, places=4)
|
||||||
self.assertEqual(len(p["scenarios"]), 4)
|
self.assertEqual(len(p["scenarios"]), 3)
|
||||||
self.assertEqual(p["scenarios"][0]["id"], "target_up")
|
self.assertEqual(p["scenarios"][0]["id"], "rr_target")
|
||||||
self.assertEqual(p["scenarios"][1]["id"], "target_down")
|
self.assertEqual(p["scenarios"][1]["id"], "expiry_flat")
|
||||||
|
|
||||||
def test_oo_legacy_single_target_still_works(self):
|
def test_oo_legacy_single_target_still_works(self):
|
||||||
a = {"opt_type": "C", "strike": 3300, "sheets": 1, "ct_mult": 0.01, "premium_paid": 5}
|
a = {"opt_type": "C", "strike": 3300, "sheets": 1, "ct_mult": 0.01, "premium_paid": 5}
|
||||||
@@ -124,6 +123,21 @@ class TestHedgePlanCalc(unittest.TestCase):
|
|||||||
self.assertEqual(p["target_price_up"], 3500)
|
self.assertEqual(p["target_price_up"], 3500)
|
||||||
self.assertEqual(p["target_price_down"], 3500)
|
self.assertEqual(p["target_price_down"], 3500)
|
||||||
|
|
||||||
|
def test_oo_legacy_up_down_rr_fields(self):
|
||||||
|
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,
|
||||||
|
index_px=3200,
|
||||||
|
leg_a=a,
|
||||||
|
leg_b=b,
|
||||||
|
)
|
||||||
|
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(p["scenarios"][0]["id"], "target_up")
|
||||||
|
self.assertEqual(p["scenarios"][1]["id"], "target_down")
|
||||||
|
|
||||||
def test_perp_short_pnl(self):
|
def test_perp_short_pnl(self):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
perp_pnl(direction="short", entry=100, exit_px=90, contracts=1, contract_size=1),
|
perp_pnl(direction="short", entry=100, exit_px=90, contracts=1, contract_size=1),
|
||||||
|
|||||||
@@ -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-1850-P"]["target_index"], 1800)
|
||||||
self.assertEqual(targets["ETH-USD_UM-260719-1890-C"]["managed_by"], "hedge_plan")
|
self.assertEqual(targets["ETH-USD_UM-260719-1890-C"]["managed_by"], "hedge_plan")
|
||||||
|
|
||||||
|
def test_active_options_targets_rr_mode_marks_managed(self):
|
||||||
|
conn = _mem()
|
||||||
|
pid = insert_plan(
|
||||||
|
conn,
|
||||||
|
{
|
||||||
|
"plan_type": "options_options",
|
||||||
|
"status": "active",
|
||||||
|
"underlying": "ETH",
|
||||||
|
"oo_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"]["managed_by"], "hedge_plan")
|
||||||
|
self.assertIsNone(targets["ETH-USD_UM-260719-1890-C"]["target_index"])
|
||||||
|
self.assertEqual(targets["ETH-USD_UM-260719-1890-C"]["oo_profit_rr"], 2.0)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user