Support dual breakout targets for options-options hedges.
Replace single S* with up/down targets across UI, preview, persist, monitor, and alerts so ranging breakouts can close the winner either way. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -324,8 +324,14 @@
|
|||||||
fillExpSelect($("hp-oo-exp-select"), d);
|
fillExpSelect($("hp-oo-exp-select"), d);
|
||||||
renderListStrikes();
|
renderListStrikes();
|
||||||
renderTStrikes();
|
renderTStrikes();
|
||||||
if (d.index_px && $("hp-target") && !$("hp-target").value) {
|
if (d.index_px) {
|
||||||
$("hp-target").value = 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-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")) $("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-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")) {
|
||||||
@@ -606,12 +613,16 @@
|
|||||||
let body;
|
let body;
|
||||||
if (isOo) {
|
if (isOo) {
|
||||||
if (!state.legA || !state.legB) throw new Error("请选用两条期权腿");
|
if (!state.legA || !state.legB) throw new Error("请选用两条期权腿");
|
||||||
const target = Number(($("hp-target") && $("hp-target").value) || 0);
|
const up = Number(($("hp-target-up") && $("hp-target-up").value) || 0);
|
||||||
if (!target) throw new Error("请填写目标价");
|
const down = Number(($("hp-target-down") && $("hp-target-down").value) || 0);
|
||||||
|
if (!up || !down) throw new Error("请填写上破与下破目标价");
|
||||||
|
if (up <= down) throw new Error("上破目标价必须大于下破目标价");
|
||||||
body = {
|
body = {
|
||||||
plan_type: "options_options",
|
plan_type: "options_options",
|
||||||
target_price: target,
|
target_price_up: up,
|
||||||
index_px: (state.chain && state.chain.index_px) || target,
|
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_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")),
|
||||||
};
|
};
|
||||||
@@ -862,6 +873,8 @@
|
|||||||
oo_expiry_loss: "期期到期亏损",
|
oo_expiry_loss: "期期到期亏损",
|
||||||
oo_expiry_win: "期期到期盈利",
|
oo_expiry_win: "期期到期盈利",
|
||||||
target_win_leg: "期期平盈利腿",
|
target_win_leg: "期期平盈利腿",
|
||||||
|
target_up_win_leg: "期期上破·平盈利腿",
|
||||||
|
target_down_win_leg: "期期下破·平盈利腿",
|
||||||
expiry: "到期",
|
expiry: "到期",
|
||||||
manual: "人工结束",
|
manual: "人工结束",
|
||||||
partial_fail: "半腿失败",
|
partial_fail: "半腿失败",
|
||||||
@@ -930,7 +943,12 @@
|
|||||||
fmt(p.perp_size, 4) +
|
fmt(p.perp_size, 4) +
|
||||||
"</div>";
|
"</div>";
|
||||||
} else {
|
} else {
|
||||||
html += "<div><span class=\"muted\">目标价 S*</span> " + fmt(p.target_price) + "</div>";
|
html +=
|
||||||
|
"<div><span class=\"muted\">目标价</span> 上破 " +
|
||||||
|
fmt(p.target_price_up || p.target_price) +
|
||||||
|
" · 下破 " +
|
||||||
|
fmt(p.target_price_down || p.target_price) +
|
||||||
|
"</div>";
|
||||||
}
|
}
|
||||||
html +=
|
html +=
|
||||||
"<div><span class=\"muted\">权利金合计</span> " +
|
"<div><span class=\"muted\">权利金合计</span> " +
|
||||||
@@ -1084,12 +1102,16 @@
|
|||||||
let body;
|
let body;
|
||||||
if (isOo) {
|
if (isOo) {
|
||||||
if (!state.legA || !state.legB) throw new Error("请选用两条期权腿");
|
if (!state.legA || !state.legB) throw new Error("请选用两条期权腿");
|
||||||
const target = Number(($("hp-target") && $("hp-target").value) || 0);
|
const up = Number(($("hp-target-up") && $("hp-target-up").value) || 0);
|
||||||
if (!target) throw new Error("请填写目标价");
|
const down = Number(($("hp-target-down") && $("hp-target-down").value) || 0);
|
||||||
|
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: target,
|
target_price_up: up,
|
||||||
|
target_price_down: down,
|
||||||
|
target_price: up,
|
||||||
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")),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3403,6 +3403,22 @@ html[data-theme="light"] .opt-be-dist-down {
|
|||||||
background: rgba(255, 255, 255, 0.03);
|
background: rgba(255, 255, 255, 0.03);
|
||||||
font-size: 0.78rem;
|
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 {
|
.hedge-plan-page-wrap .hp-contracts-cell {
|
||||||
max-width: 220px;
|
max-width: 220px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@@ -223,12 +223,14 @@ def _hedge_ratio(opt_pnl: float, perp_pnl: float) -> Optional[float]:
|
|||||||
|
|
||||||
def build_options_options_preview(
|
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,
|
index_px: float,
|
||||||
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]:
|
||||||
"""期期情景:目标价 / 到期现价 / 到期两边."""
|
"""期期情景:上破/下破目标价 / 到期现价 / 最大保费损耗."""
|
||||||
|
|
||||||
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(
|
||||||
@@ -240,11 +242,25 @@ 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 填两边
|
||||||
|
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)
|
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)
|
a_up = _leg_pnl(leg_a, up_f)
|
||||||
b_t = _leg_pnl(leg_b, target_price)
|
b_up = _leg_pnl(leg_b, up_f)
|
||||||
at_target = a_t + b_t
|
at_up = a_up + b_up
|
||||||
win_leg = "a" if a_t >= b_t else "b"
|
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)
|
a_flat = _leg_pnl(leg_a, index_px)
|
||||||
b_flat = _leg_pnl(leg_b, index_px)
|
b_flat = _leg_pnl(leg_b, index_px)
|
||||||
flat_total = a_flat + b_flat
|
flat_total = a_flat + b_flat
|
||||||
@@ -253,17 +269,30 @@ def build_options_options_preview(
|
|||||||
return {
|
return {
|
||||||
"plan_type": "options_options",
|
"plan_type": "options_options",
|
||||||
"premium_paid": round(prem, 6),
|
"premium_paid": round(prem, 6),
|
||||||
"target_price": target_price,
|
"target_price": up_f, # 兼容旧字段,取上破
|
||||||
"winner_at_target": win_leg,
|
"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": [
|
"scenarios": [
|
||||||
{
|
{
|
||||||
"id": "target",
|
"id": "target_up",
|
||||||
"label": "到达目标价",
|
"label": "上破目标",
|
||||||
"spot": target_price,
|
"spot": up_f,
|
||||||
"leg_a_pnl": round(a_t, 4),
|
"leg_a_pnl": round(a_up, 4),
|
||||||
"leg_b_pnl": round(b_t, 4),
|
"leg_b_pnl": round(b_up, 4),
|
||||||
"total": round(at_target, 4),
|
"total": round(at_up, 4),
|
||||||
"note": f"盈利方≈腿{win_leg.upper()}(可平);亏损方默认到期",
|
"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",
|
"id": "expiry_flat",
|
||||||
@@ -285,9 +314,11 @@ def build_options_options_preview(
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"summary": {
|
"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),
|
"expiry_flat_total": round(expiry_loss, 4),
|
||||||
"premium_paid": round(prem, 4),
|
"premium_paid": round(prem, 6),
|
||||||
"expiry_is_loss": flat_total <= 0,
|
"expiry_is_loss": flat_total <= 0,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,20 @@ def init_hedge_plan_tables(conn: sqlite3.Connection) -> None:
|
|||||||
conn.execute(
|
conn.execute(
|
||||||
"CREATE INDEX IF NOT EXISTS idx_hedge_plan_legs_plan ON hedge_plan_legs(plan_id)"
|
"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:
|
def count_active_plans(conn: sqlite3.Connection, plan_type: Optional[str] = None) -> int:
|
||||||
|
|||||||
@@ -235,12 +235,29 @@ def _tick_po(cfg: dict[str, Any], conn: Any, plan: dict[str, Any], legs: list[di
|
|||||||
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]]:
|
||||||
target = _sf(plan.get("target_price"))
|
"""期期:触及上破或下破目标价时平盈利腿."""
|
||||||
idx = _index_px(cfg, str(plan.get("underlying") or "ETH"))
|
idx = _index_px(cfg, str(plan.get("underlying") or "ETH"))
|
||||||
if target is None or idx is None:
|
if idx is None:
|
||||||
return None
|
return None
|
||||||
near = abs(idx - target) / max(abs(target), 1.0) <= 0.002
|
up = _sf(plan.get("target_price_up"))
|
||||||
if not near:
|
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
|
return None
|
||||||
if not _env_bool("HEDGE_PLAN_OO_CLOSE_WINNER_ONLY", True):
|
if not _env_bool("HEDGE_PLAN_OO_CLOSE_WINNER_ONLY", True):
|
||||||
return None
|
return None
|
||||||
@@ -270,16 +287,23 @@ def _tick_oo_target(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
return {"plan_id": plan["id"], "msg": "平盈利腿失败", "close": close_r}
|
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(
|
conn.execute(
|
||||||
"UPDATE hedge_plan_legs SET status=?, close_reason=?, closed_at=?, realized_pnl=? WHERE id=?",
|
"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 = dict(plan)
|
||||||
mid["close_reason"] = "target_win_leg"
|
mid["close_reason"] = reason
|
||||||
mid["status"] = "active"
|
mid["status"] = "active"
|
||||||
notify_plan_end(cfg, conn, mid)
|
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(
|
def _tick_oo_expiry(
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ def build_hedge_start_message(plan: dict[str, Any], *, legs: Optional[list[dict[
|
|||||||
else:
|
else:
|
||||||
lines.extend(
|
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",
|
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_tp": "永续止盈(期权默认不平)",
|
||||||
"perp_sl": "永续止损(期权强制平)",
|
"perp_sl": "永续止损(期权强制平)",
|
||||||
"target_win_leg": "期期已平盈利腿(中间态)",
|
"target_win_leg": "期期已平盈利腿(中间态)",
|
||||||
|
"target_up_win_leg": "期期上破·已平盈利腿",
|
||||||
|
"target_down_win_leg": "期期下破·已平盈利腿",
|
||||||
"oo_expiry_loss": "期期到期无盈利·总亏损",
|
"oo_expiry_loss": "期期到期无盈利·总亏损",
|
||||||
"oo_expiry_win": "期期到期仍盈利",
|
"oo_expiry_win": "期期到期仍盈利",
|
||||||
"expiry": "到期收口",
|
"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):
|
if int(plan.get("wechat_end_sent") or 0):
|
||||||
return False
|
return False
|
||||||
# 中间态 target_win_leg 不算正式结束推送(用告警)
|
# 中间态 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(
|
notify_hedge(
|
||||||
cfg,
|
cfg,
|
||||||
build_hedge_alert_message(
|
build_hedge_alert_message(
|
||||||
title="期期已平盈利腿,亏损腿继续持有至到期",
|
title=f"期期{side}已平盈利腿,亏损腿继续持有至到期",
|
||||||
plan_id=plan.get("id"),
|
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
|
return True
|
||||||
|
|||||||
@@ -382,8 +382,20 @@ 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 "请选用两条期权腿"
|
||||||
if body.get("target_price") in (None, ""):
|
up = body.get("target_price_up")
|
||||||
return "缺少目标价"
|
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 None
|
||||||
return "未知计划类型"
|
return "未知计划类型"
|
||||||
|
|
||||||
|
|||||||
@@ -269,7 +269,21 @@ def _persist_oo(cfg: dict[str, Any], result: dict[str, Any], body: dict[str, Any
|
|||||||
"plan_type": "options_options",
|
"plan_type": "options_options",
|
||||||
"status": "active",
|
"status": "active",
|
||||||
"underlying": str(body.get("underlying") or "ETH").upper(),
|
"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(),
|
"sizing_mode_at_open": load_position_sizing_mode(),
|
||||||
"premium_total": premium,
|
"premium_total": premium,
|
||||||
"opened_at": result.get("opened_at"),
|
"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]:
|
def _preview_oo(body: dict[str, Any]) -> dict[str, Any]:
|
||||||
target = float(body["target_price"])
|
up = body.get("target_price_up")
|
||||||
index_px = float(body.get("index_px") or target)
|
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_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)):
|
||||||
@@ -626,7 +652,8 @@ 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} 权利金")
|
||||||
return build_options_options_preview(
|
return build_options_options_preview(
|
||||||
target_price=target,
|
target_price_up=up_f,
|
||||||
|
target_price_down=down_f,
|
||||||
index_px=index_px,
|
index_px=index_px,
|
||||||
leg_a=leg_a,
|
leg_a=leg_a,
|
||||||
leg_b=leg_b,
|
leg_b=leg_b,
|
||||||
|
|||||||
@@ -118,11 +118,14 @@
|
|||||||
<div class="form-row hp-uly-row">
|
<div class="form-row hp-uly-row">
|
||||||
<button type="button" class="btn-secondary hp-uly-btn-oo active" data-uly="ETH">ETH</button>
|
<button type="button" class="btn-secondary hp-uly-btn-oo active" data-uly="ETH">ETH</button>
|
||||||
<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>
|
||||||
<label>目标价 S* <input type="number" step="any" id="hp-target" /></label>
|
</div>
|
||||||
|
<div class="form-row hp-target-row">
|
||||||
|
<label>上破目标 <input type="number" step="any" id="hp-target-up" placeholder="向上突破" /></label>
|
||||||
|
<label>下破目标 <input type="number" step="any" id="hp-target-down" placeholder="向下突破" /></label>
|
||||||
</div>
|
</div>
|
||||||
<div id="hp-oo-index" class="muted hp-quote-line"></div>
|
<div id="hp-oo-index" class="muted hp-quote-line"></div>
|
||||||
<div id="hp-oo-bal-line" class="muted hp-quote-line"></div>
|
<div id="hp-oo-bal-line" class="muted hp-quote-line"></div>
|
||||||
<p class="muted hp-unit-hint">单位说明:目标价=指数价(USD) · 张数=<strong>期权张</strong>(整张) · 权利金结算币=USDC</p>
|
<p class="muted hp-unit-hint">震荡突破:设上下两个目标价(USD);触达任一侧重平盈利腿。张数=<strong>期权张</strong> · 权利金=USDC</p>
|
||||||
<div id="hp-oo-legs" class="hp-oo-legs">
|
<div id="hp-oo-legs" class="hp-oo-legs">
|
||||||
<div class="hp-oo-leg-row" data-leg="a">
|
<div class="hp-oo-leg-row" data-leg="a">
|
||||||
<div class="muted" id="hp-oo-leg-a-info">腿A: 尚未选用</div>
|
<div class="muted" id="hp-oo-leg-a-info">腿A: 尚未选用</div>
|
||||||
@@ -186,7 +189,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="hp-result-tbody-oo">
|
<tbody id="hp-result-tbody-oo">
|
||||||
<tr><td colspan="6" class="muted">选用两腿并填目标价后点计算</td></tr>
|
<tr><td colspan="6" class="muted">选用两腿并填上破/下破目标后点计算</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -230,4 +233,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/static/hedge_plan.js?v=11"></script>
|
<script src="/static/hedge_plan.js?v=12"></script>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<link rel="stylesheet" href="/static/instance_theme_early.css?v=4">
|
<link rel="stylesheet" href="/static/instance_theme_early.css?v=4">
|
||||||
<link rel="stylesheet" href="/static/account_risk_badge.css?v=4">
|
<link rel="stylesheet" href="/static/account_risk_badge.css?v=4">
|
||||||
<link rel="stylesheet" href="/static/instance_page.css?v=6">
|
<link rel="stylesheet" href="/static/instance_page.css?v=6">
|
||||||
<link rel="stylesheet" href="/static/instance_theme.css?v=89">
|
<link rel="stylesheet" href="/static/instance_theme.css?v=90">
|
||||||
<script src="/static/account_risk_badge.js?v=4"></script>
|
<script src="/static/account_risk_badge.js?v=4"></script>
|
||||||
<meta name="theme-color" content="#0b0d14">
|
<meta name="theme-color" content="#0b0d14">
|
||||||
<title>{{ exchange_display }} · 加密货币 | 交易监控复盘系统</title>
|
<title>{{ exchange_display }} · 加密货币 | 交易监控复盘系统</title>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<link rel="manifest" href="/static/icons/manifest.webmanifest">
|
<link rel="manifest" href="/static/icons/manifest.webmanifest">
|
||||||
<title>{{ exchange_display }} · 加密货币 | 交易监控复盘系统</title>
|
<title>{{ exchange_display }} · 加密货币 | 交易监控复盘系统</title>
|
||||||
<link rel="stylesheet" href="/static/instance_page.css?v=3">
|
<link rel="stylesheet" href="/static/instance_page.css?v=3">
|
||||||
<link rel="stylesheet" href="/static/instance_theme.css?v=89">
|
<link rel="stylesheet" href="/static/instance_theme.css?v=90">
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body
|
<body
|
||||||
|
|||||||
@@ -72,9 +72,25 @@ class TestHedgePlanCalc(unittest.TestCase):
|
|||||||
def test_oo_expiry_loss_flag(self):
|
def test_oo_expiry_loss_flag(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}
|
||||||
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(target_price=3500, index_px=3200, leg_a=a, leg_b=b)
|
p = build_options_options_preview(
|
||||||
|
target_price_up=3500,
|
||||||
|
target_price_down=3000,
|
||||||
|
index_px=3200,
|
||||||
|
leg_a=a,
|
||||||
|
leg_b=b,
|
||||||
|
)
|
||||||
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(len(p["scenarios"]), 4)
|
||||||
|
self.assertEqual(p["scenarios"][0]["id"], "target_up")
|
||||||
|
self.assertEqual(p["scenarios"][1]["id"], "target_down")
|
||||||
|
|
||||||
|
def test_oo_legacy_single_target_still_works(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=3500, index_px=3200, leg_a=a, leg_b=b)
|
||||||
|
self.assertEqual(p["target_price_up"], 3500)
|
||||||
|
self.assertEqual(p["target_price_down"], 3500)
|
||||||
|
|
||||||
def test_perp_short_pnl(self):
|
def test_perp_short_pnl(self):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
|
|||||||
@@ -148,6 +148,8 @@ class TestHedgePlanOrderPath(unittest.TestCase):
|
|||||||
}
|
}
|
||||||
body = {
|
body = {
|
||||||
"target_price": 1900,
|
"target_price": 1900,
|
||||||
|
"target_price_up": 1950,
|
||||||
|
"target_price_down": 1750,
|
||||||
"leg_a": {"inst_id": "A", "sheets": 1},
|
"leg_a": {"inst_id": "A", "sheets": 1},
|
||||||
"leg_b": {"inst_id": "B", "sheets": 1},
|
"leg_b": {"inst_id": "B", "sheets": 1},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user