diff --git a/lib/common/static/hedge_plan.js b/lib/common/static/hedge_plan.js
index 7918c65..a567855 100644
--- a/lib/common/static/hedge_plan.js
+++ b/lib/common/static/hedge_plan.js
@@ -324,8 +324,14 @@
fillExpSelect($("hp-oo-exp-select"), d);
renderListStrikes();
renderTStrikes();
- if (d.index_px && $("hp-target") && !$("hp-target").value) {
- $("hp-target").value = d.index_px;
+ 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));
+ }
}
}
@@ -583,7 +589,8 @@
if ($("hp-contracts")) $("hp-contracts").value = "";
if ($("hp-tp")) $("hp-tp").value = "";
if ($("hp-sl")) $("hp-sl").value = "";
- if ($("hp-target")) $("hp-target").value = "";
+ if ($("hp-target-up")) $("hp-target-up").value = "";
+ if ($("hp-target-down")) $("hp-target-down").value = "";
if ($("hp-sel-inst")) $("hp-sel-inst").textContent = "—";
if ($("hp-premium-line")) $("hp-premium-line").textContent = "";
if ($("hp-oo-sheets-a")) {
@@ -606,12 +613,16 @@
let body;
if (isOo) {
if (!state.legA || !state.legB) throw new Error("请选用两条期权腿");
- const target = Number(($("hp-target") && $("hp-target").value) || 0);
- if (!target) 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("上破目标价必须大于下破目标价");
body = {
plan_type: "options_options",
- target_price: target,
- index_px: (state.chain && state.chain.index_px) || target,
+ target_price_up: up,
+ target_price_down: down,
+ target_price: up,
+ index_px: (state.chain && state.chain.index_px) || (up + down) / 2,
leg_a: legPayload(state.legA, ooSheets("hp-oo-sheets-a")),
leg_b: legPayload(state.legB, ooSheets("hp-oo-sheets-b")),
};
@@ -862,6 +873,8 @@
oo_expiry_loss: "期期到期亏损",
oo_expiry_win: "期期到期盈利",
target_win_leg: "期期平盈利腿",
+ target_up_win_leg: "期期上破·平盈利腿",
+ target_down_win_leg: "期期下破·平盈利腿",
expiry: "到期",
manual: "人工结束",
partial_fail: "半腿失败",
@@ -930,7 +943,12 @@
fmt(p.perp_size, 4) +
"";
} else {
- html += "
权利金合计 " +
@@ -1084,12 +1102,16 @@
let body;
if (isOo) {
if (!state.legA || !state.legB) throw new Error("请选用两条期权腿");
- const target = Number(($("hp-target") && $("hp-target").value) || 0);
- if (!target) 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("上破目标价必须大于下破目标价");
body = {
plan_type: "options_options",
underlying: state.underlying,
- target_price: target,
+ target_price_up: up,
+ target_price_down: down,
+ target_price: up,
leg_a: legPayload(state.legA, ooSheets("hp-oo-sheets-a")),
leg_b: legPayload(state.legB, ooSheets("hp-oo-sheets-b")),
};
diff --git a/lib/common/static/instance_theme.css b/lib/common/static/instance_theme.css
index 8678072..2539613 100644
--- a/lib/common/static/instance_theme.css
+++ b/lib/common/static/instance_theme.css
@@ -3403,6 +3403,22 @@ html[data-theme="light"] .opt-be-dist-down {
background: rgba(255, 255, 255, 0.03);
font-size: 0.78rem;
}
+.hedge-plan-page-wrap .hp-target-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+ align-items: center;
+ margin: 6px 0;
+}
+.hedge-plan-page-wrap .hp-target-row label {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 0.8rem;
+}
+.hedge-plan-page-wrap .hp-target-row input {
+ width: 110px;
+}
.hedge-plan-page-wrap .hp-contracts-cell {
max-width: 220px;
overflow: hidden;
diff --git a/lib/hedge_plan/hedge_plan_calc_lib.py b/lib/hedge_plan/hedge_plan_calc_lib.py
index 130cc21..a4fae7d 100644
--- a/lib/hedge_plan/hedge_plan_calc_lib.py
+++ b/lib/hedge_plan/hedge_plan_calc_lib.py
@@ -223,12 +223,14 @@ def _hedge_ratio(opt_pnl: float, perp_pnl: float) -> Optional[float]:
def build_options_options_preview(
*,
- target_price: float,
+ target_price: float | None = None,
+ target_price_up: float | None = None,
+ target_price_down: float | None = None,
index_px: float,
leg_a: dict[str, Any],
leg_b: dict[str, Any],
) -> dict[str, Any]:
- """期期情景:目标价 / 到期现价 / 到期两边."""
+ """期期情景:上破/下破目标价 / 到期现价 / 最大保费损耗."""
def _leg_pnl(leg: dict[str, Any], spot: float) -> float:
return option_expiry_pnl(
@@ -240,11 +242,25 @@ def build_options_options_preview(
premium_paid=float(leg.get("premium_paid") or 0),
)
+ # 兼容旧单目标:若未传上下目标则用 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("缺少上破/下破目标价")
+ 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_t = _leg_pnl(leg_a, target_price)
- b_t = _leg_pnl(leg_b, target_price)
- at_target = a_t + b_t
- win_leg = "a" if a_t >= b_t else "b"
+ a_up = _leg_pnl(leg_a, up_f)
+ b_up = _leg_pnl(leg_b, up_f)
+ at_up = a_up + b_up
+ win_up = "a" if a_up >= b_up else "b"
+
+ a_dn = _leg_pnl(leg_a, down_f)
+ b_dn = _leg_pnl(leg_b, down_f)
+ at_dn = a_dn + b_dn
+ 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
@@ -253,17 +269,30 @@ def build_options_options_preview(
return {
"plan_type": "options_options",
"premium_paid": round(prem, 6),
- "target_price": target_price,
- "winner_at_target": win_leg,
+ "target_price": up_f, # 兼容旧字段,取上破
+ "target_price_up": up_f,
+ "target_price_down": down_f,
+ "winner_at_up": win_up,
+ "winner_at_down": win_dn,
+ "winner_at_target": win_up,
"scenarios": [
{
- "id": "target",
- "label": "到达目标价",
- "spot": target_price,
- "leg_a_pnl": round(a_t, 4),
- "leg_b_pnl": round(b_t, 4),
- "total": round(at_target, 4),
- "note": f"盈利方≈腿{win_leg.upper()}(可平);亏损方默认到期",
+ "id": "target_up",
+ "label": "上破目标",
+ "spot": up_f,
+ "leg_a_pnl": round(a_up, 4),
+ "leg_b_pnl": round(b_up, 4),
+ "total": round(at_up, 4),
+ "note": f"盈利方≈腿{win_up.upper()}(可平);亏损方默认到期",
+ },
+ {
+ "id": "target_down",
+ "label": "下破目标",
+ "spot": down_f,
+ "leg_a_pnl": round(a_dn, 4),
+ "leg_b_pnl": round(b_dn, 4),
+ "total": round(at_dn, 4),
+ "note": f"盈利方≈腿{win_dn.upper()}(可平);亏损方默认到期",
},
{
"id": "expiry_flat",
@@ -285,9 +314,11 @@ def build_options_options_preview(
},
],
"summary": {
- "at_target_total": round(at_target, 4),
+ "at_target_up_total": round(at_up, 4),
+ "at_target_down_total": round(at_dn, 4),
+ "at_target_total": round(at_up, 4),
"expiry_flat_total": round(expiry_loss, 4),
- "premium_paid": round(prem, 4),
+ "premium_paid": round(prem, 6),
"expiry_is_loss": flat_total <= 0,
},
}
diff --git a/lib/hedge_plan/hedge_plan_db.py b/lib/hedge_plan/hedge_plan_db.py
index c8b1052..9ce5a0b 100644
--- a/lib/hedge_plan/hedge_plan_db.py
+++ b/lib/hedge_plan/hedge_plan_db.py
@@ -70,6 +70,20 @@ def init_hedge_plan_tables(conn: sqlite3.Connection) -> None:
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_hedge_plan_legs_plan ON hedge_plan_legs(plan_id)"
)
+ _ensure_column(conn, "hedge_plans", "target_price_up", "REAL")
+ _ensure_column(conn, "hedge_plans", "target_price_down", "REAL")
+
+
+def _ensure_column(conn: sqlite3.Connection, table: str, col: str, typedef: str) -> None:
+ rows = conn.execute(f"PRAGMA table_info({table})").fetchall()
+ names: set[str] = set()
+ for r in rows:
+ try:
+ names.add(str(r["name"]))
+ except (TypeError, KeyError, IndexError):
+ names.add(str(r[1]))
+ if col not in names:
+ conn.execute(f"ALTER TABLE {table} ADD COLUMN {col} {typedef}")
def count_active_plans(conn: sqlite3.Connection, plan_type: Optional[str] = None) -> int:
diff --git a/lib/hedge_plan/hedge_plan_monitor_lib.py b/lib/hedge_plan/hedge_plan_monitor_lib.py
index 4255473..1048ea8 100644
--- a/lib/hedge_plan/hedge_plan_monitor_lib.py
+++ b/lib/hedge_plan/hedge_plan_monitor_lib.py
@@ -235,12 +235,29 @@ def _tick_po(cfg: dict[str, Any], conn: Any, plan: dict[str, Any], legs: list[di
def _tick_oo_target(
cfg: dict[str, Any], conn: Any, plan: dict[str, Any], legs: list[dict[str, Any]]
) -> Optional[dict[str, Any]]:
- target = _sf(plan.get("target_price"))
+ """期期:触及上破或下破目标价时平盈利腿."""
idx = _index_px(cfg, str(plan.get("underlying") or "ETH"))
- if target is None or idx is None:
+ if idx is None:
return None
- near = abs(idx - target) / max(abs(target), 1.0) <= 0.002
- if not near:
+ up = _sf(plan.get("target_price_up"))
+ down = _sf(plan.get("target_price_down"))
+ # 旧计划仅有单目标:两边都用它
+ legacy = _sf(plan.get("target_price"))
+ if up is None and legacy is not None:
+ up = legacy
+ if down is None and legacy is not None:
+ down = legacy
+ if up is None and down is None:
+ 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:
return None
if not _env_bool("HEDGE_PLAN_OO_CLOSE_WINNER_ONLY", True):
return None
@@ -270,16 +287,23 @@ 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"
conn.execute(
"UPDATE hedge_plan_legs SET status=?, close_reason=?, closed_at=?, realized_pnl=? WHERE id=?",
- ("closed", "target_win_leg", _now(), best_pnl, best["id"]),
+ ("closed", reason, _now(), best_pnl, best["id"]),
)
- update_plan(conn, int(plan["id"]), close_reason="target_win_leg")
+ update_plan(conn, int(plan["id"]), close_reason=reason)
mid = dict(plan)
- mid["close_reason"] = "target_win_leg"
+ mid["close_reason"] = reason
mid["status"] = "active"
notify_plan_end(cfg, conn, mid)
- return {"plan_id": plan["id"], "close_reason": "target_win_leg", "closed_leg": best.get("id")}
+ return {
+ "plan_id": plan["id"],
+ "close_reason": reason,
+ "hit_side": hit_side,
+ "closed_leg": best.get("id"),
+ "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 a5f8736..76a13a3 100644
--- a/lib/hedge_plan/hedge_plan_notify_lib.py
+++ b/lib/hedge_plan/hedge_plan_notify_lib.py
@@ -48,7 +48,8 @@ def build_hedge_start_message(plan: dict[str, Any], *, legs: Optional[list[dict[
else:
lines.extend(
[
- f"🎯 目标价 S*:{_fmt(plan.get('target_price'))}",
+ 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",
]
)
@@ -78,6 +79,8 @@ def build_hedge_end_message(plan: dict[str, Any]) -> str:
"perp_tp": "永续止盈(期权默认不平)",
"perp_sl": "永续止损(期权强制平)",
"target_win_leg": "期期已平盈利腿(中间态)",
+ "target_up_win_leg": "期期上破·已平盈利腿",
+ "target_down_win_leg": "期期下破·已平盈利腿",
"oo_expiry_loss": "期期到期无盈利·总亏损",
"oo_expiry_win": "期期到期仍盈利",
"expiry": "到期收口",
@@ -143,13 +146,23 @@ def notify_plan_end(cfg: dict[str, Any], conn: Any, plan: dict[str, Any]) -> boo
if int(plan.get("wechat_end_sent") or 0):
return False
# 中间态 target_win_leg 不算正式结束推送(用告警)
- if (plan.get("close_reason") or "") == "target_win_leg" and (plan.get("status") or "") != "closed":
+ if (plan.get("close_reason") or "") in (
+ "target_win_leg",
+ "target_up_win_leg",
+ "target_down_win_leg",
+ ) 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 "目标价"
+ )
notify_hedge(
cfg,
build_hedge_alert_message(
- title="期期已平盈利腿,亏损腿继续持有至到期",
+ title=f"期期{side}已平盈利腿,亏损腿继续持有至到期",
plan_id=plan.get("id"),
- detail=f"目标价 {_fmt(plan.get('target_price'))}",
+ 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'))}"
+ ),
),
)
return True
diff --git a/lib/hedge_plan/hedge_plan_orders_lib.py b/lib/hedge_plan/hedge_plan_orders_lib.py
index 9a335ea..e4635db 100644
--- a/lib/hedge_plan/hedge_plan_orders_lib.py
+++ b/lib/hedge_plan/hedge_plan_orders_lib.py
@@ -382,8 +382,20 @@ 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 "请选用两条期权腿"
- if body.get("target_price") in (None, ""):
- 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 "目标价无效"
return None
return "未知计划类型"
diff --git a/lib/hedge_plan/hedge_plan_register.py b/lib/hedge_plan/hedge_plan_register.py
index 617604d..0c17f83 100644
--- a/lib/hedge_plan/hedge_plan_register.py
+++ b/lib/hedge_plan/hedge_plan_register.py
@@ -269,7 +269,21 @@ def _persist_oo(cfg: dict[str, Any], result: dict[str, Any], body: dict[str, Any
"plan_type": "options_options",
"status": "active",
"underlying": str(body.get("underlying") or "ETH").upper(),
- "target_price": float(body.get("target_price") or 0),
+ "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
+ ),
"sizing_mode_at_open": load_position_sizing_mode(),
"premium_total": premium,
"opened_at": result.get("opened_at"),
@@ -610,8 +624,20 @@ def _preview_po(body: dict[str, Any]) -> dict[str, Any]:
def _preview_oo(body: dict[str, Any]) -> dict[str, Any]:
- target = float(body["target_price"])
- index_px = float(body.get("index_px") or target)
+ 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("请填写上破与下破目标价")
+ up_f = float(up)
+ down_f = float(down)
+ if up_f <= down_f:
+ raise ValueError("上破目标价必须大于下破目标价")
+ index_px = float(body.get("index_px") or ((up_f + down_f) / 2))
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)):
@@ -626,7 +652,8 @@ def _preview_oo(body: dict[str, Any]) -> dict[str, Any]:
if leg.get("premium_paid") is None:
raise ValueError(f"缺少 {name} 权利金")
return build_options_options_preview(
- target_price=target,
+ target_price_up=up_f,
+ target_price_down=down_f,
index_px=index_px,
leg_a=leg_a,
leg_b=leg_b,
diff --git a/lib/hedge_plan/templates/hedge_plan_panel.html b/lib/hedge_plan/templates/hedge_plan_panel.html
index a0393d8..bec5c75 100644
--- a/lib/hedge_plan/templates/hedge_plan_panel.html
+++ b/lib/hedge_plan/templates/hedge_plan_panel.html
@@ -118,11 +118,14 @@
-
+
+
+
+
-
单位说明:目标价=指数价(USD) · 张数=期权张(整张) · 权利金结算币=USDC
+
震荡突破:设上下两个目标价(USD);触达任一侧重平盈利腿。张数=期权张 · 权利金=USDC
腿A: 尚未选用
@@ -186,7 +189,7 @@
- | 选用两腿并填目标价后点计算 |
+ | 选用两腿并填上破/下破目标后点计算 |
@@ -230,4 +233,4 @@
-
+
diff --git a/lib/instance/templates/embed_shell.html b/lib/instance/templates/embed_shell.html
index f83cf80..0791ce0 100644
--- a/lib/instance/templates/embed_shell.html
+++ b/lib/instance/templates/embed_shell.html
@@ -7,7 +7,7 @@