diff --git a/lib/common/static/hedge_plan.js b/lib/common/static/hedge_plan.js index faf5904..92ba5ae 100644 --- a/lib/common/static/hedge_plan.js +++ b/lib/common/static/hedge_plan.js @@ -904,7 +904,7 @@ "\">" + contracts + "" + - (p.status || "") + + '进行中' + "" + activeTargetLabel(p) + "" + diff --git a/lib/common/static/instance_theme.css b/lib/common/static/instance_theme.css index 7e5bf2b..95bb521 100644 --- a/lib/common/static/instance_theme.css +++ b/lib/common/static/instance_theme.css @@ -3238,6 +3238,10 @@ html[data-theme="light"] .opt-be-dist-down { .hedge-plan-page-wrap .hp-pnl-pos { color: #7ee787; } +.hedge-plan-page-wrap .hp-plan-active { + color: #7ee787; + font-weight: 700; +} .hedge-plan-page-wrap .hp-pnl-neg { color: #ff8a8a; } @@ -4255,6 +4259,15 @@ html[data-theme="light"] .options-stats-pnl-summary .options-stat-item { color: #9ad0ff; font-variant-numeric: tabular-nums; } +.options-page-wrap .opt-target-row--managed { + border-color: rgba(126, 231, 135, 0.38); + background: rgba(46, 160, 67, 0.08); +} +.options-page-wrap .opt-target-row--managed .opt-target-armed, +.options-page-wrap .opt-target-mon-managed { + color: #7ee787; + font-weight: 600; +} .options-page-wrap .opt-target-est { display: inline-flex; flex-wrap: wrap; @@ -4320,6 +4333,13 @@ html[data-theme="light"] .options-stats-pnl-summary .options-stat-item { padding: 2px 8px; font-size: 0.7rem; } +.opt-target-mon-item--managed { + border-color: rgba(126, 231, 135, 0.28); +} +.opt-target-mon-managed { + margin-left: auto; + font-size: 0.72rem; +} .options-page-wrap .opt-bid-plain { color: #dbe6ff; font-variant-numeric: tabular-nums; diff --git a/lib/common/static/options_panel.js b/lib/common/static/options_panel.js index 2a0dd53..565183b 100644 --- a/lib/common/static/options_panel.js +++ b/lib/common/static/options_panel.js @@ -1217,6 +1217,23 @@ function renderTargetDelegateRow(p) { const inst = p.inst_id || ""; + const hedgeTarget = p.hedge_plan_target || null; + if (hedgeTarget && Number(hedgeTarget.target_index) > 0) { + const side = (p.opt_type || hedgeTarget.opt_type || "").toUpperCase() === "P" ? "Put ≤" : "Call ≥"; + return ( + '
' + + '对冲计划' + + '计划 #' + + hedgeTarget.plan_id + + " · " + + side + + " " + + fmt(hedgeTarget.target_index, 1) + + "" + + '进行中 · 由对冲计划监控,到位后仅平盈利腿' + + "
" + ); + } const tgt = p.target_index != null && p.target_index !== "" ? Number(p.target_index) : null; const armed = tgt != null && Number.isFinite(tgt) && tgt > 0; const ethAmt = posEthAmount(p); @@ -1458,11 +1475,14 @@ box.hidden = false; host.innerHTML = rows.map(function (t) { const side = (t.opt_type || "").toUpperCase() === "P" ? "Put ≤" : "Call ≥"; + const managed = t.managed_by === "hedge_plan"; return ( - '
' + + '
' + '' + (t.inst_id || "") + "" + '' + side + " " + fmt(t.target_index, 1) + "" + - '' + + (managed + ? '对冲计划 #' + (t.plan_id || "") + " · 进行中" + : '') + "
" ); }).join(""); @@ -1634,16 +1654,28 @@ if (seq !== positionsRefreshSeq) return; const list = resolvePositionsList(d); paintPositions(list); - const fromPos = list - .filter(function (p) { return p && p.target_index != null; }) - .map(function (p) { - return { + const fromPos = list.reduce(function (targets, p) { + if (!p) return targets; + if (p.target_index != null) { + targets.push({ id: p.target_monitor_id, inst_id: p.inst_id, opt_type: p.opt_type, target_index: p.target_index, - }; - }); + }); + } + const hedgeTarget = p.hedge_plan_target; + if (hedgeTarget && hedgeTarget.target_index != null) { + targets.push({ + inst_id: p.inst_id, + opt_type: p.opt_type || hedgeTarget.opt_type, + target_index: hedgeTarget.target_index, + plan_id: hedgeTarget.plan_id, + managed_by: hedgeTarget.managed_by, + }); + } + return targets; + }, []); if (fromPos.length) { paintTargetMonitors(fromPos); } else { diff --git a/lib/hedge_plan/hedge_plan_db.py b/lib/hedge_plan/hedge_plan_db.py index 9ce5a0b..b615429 100644 --- a/lib/hedge_plan/hedge_plan_db.py +++ b/lib/hedge_plan/hedge_plan_db.py @@ -205,6 +205,45 @@ def attach_legs_to_plans(conn: sqlite3.Connection, plans: list[dict[str, Any]]) return out +def active_options_targets_by_inst(conn: sqlite3.Connection) -> dict[str, dict[str, Any]]: + """返回由进行中「期期对冲」托管的期权目标位,仅供期权页只读展示。 + + 这些目标由 hedge_plan_monitor_lib 执行,绝不能写入 options_target_monitors, + 否则两套监控会同时尝试平掉同一条期权腿。 + """ + 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 + FROM hedge_plans p + JOIN hedge_plan_legs l ON l.plan_id = p.id + WHERE p.plan_type = 'options_options' + AND p.status IN ('opening', 'active', 'partial') + AND l.status = 'open' + AND l.inst_id IS NOT NULL + AND l.inst_id != '' + ORDER BY p.id DESC, l.id DESC + """ + ).fetchall() + out: dict[str, dict[str, Any]] = {} + for raw in rows: + row = dict(raw) + inst_id = str(row.get("inst_id") or "") + opt_type = str(row.get("opt_type") or "").upper() + 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: + continue + out[inst_id] = { + "plan_id": int(row["plan_id"]), + "underlying": row.get("underlying"), + "opt_type": opt_type, + "target_index": target_f, + "managed_by": "hedge_plan", + } + return out + + def _sf(v: Any) -> Optional[float]: try: if v is None or v == "": diff --git a/lib/options/options_hub_lib.py b/lib/options/options_hub_lib.py index 9d60786..807f5a4 100644 --- a/lib/options/options_hub_lib.py +++ b/lib/options/options_hub_lib.py @@ -34,16 +34,21 @@ def build_options_hub_snapshot(cfg: dict[str, Any]) -> dict[str, Any]: try: conn = cfg["get_db"]() try: + from lib.hedge_plan.hedge_plan_db import active_options_targets_by_inst from lib.options.options_target_lib import list_active_targets, list_closing_targets, targets_by_inst target_monitors = list_active_targets(conn) + list_closing_targets(conn) tgt_map = targets_by_inst(conn) + hedge_target_map = active_options_targets_by_inst(conn) for p in positions: mon = tgt_map.get(str(p.get("inst_id") or "")) if mon: p["target_index"] = mon.get("target_index") p["target_monitor_id"] = mon.get("id") p["target_monitor"] = mon + hedge_target = hedge_target_map.get(str(p.get("inst_id") or "")) + if hedge_target: + p["hedge_plan_target"] = hedge_target finally: conn.close() except Exception: diff --git a/lib/options/options_register.py b/lib/options/options_register.py index 00ea89c..ccfe1f2 100644 --- a/lib/options/options_register.py +++ b/lib/options/options_register.py @@ -739,8 +739,10 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None: conn = cfg["get_db"]() try: from lib.options.options_target_lib import targets_by_inst + from lib.hedge_plan.hedge_plan_db import active_options_targets_by_inst tgt_map = targets_by_inst(conn) + hedge_target_map = active_options_targets_by_inst(conn) rows = [] for p in raw: inst = str(p.get("instId") or "").strip() @@ -758,6 +760,9 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None: row["target_index"] = mon.get("target_index") row["target_monitor_id"] = mon.get("id") row["target_monitor"] = mon + hedge_target = hedge_target_map.get(inst) + if hedge_target: + row["hedge_plan_target"] = hedge_target rows.append(row) finally: conn.close() diff --git a/lib/options/templates/options_panel.html b/lib/options/templates/options_panel.html index 6d1a5a6..7d22ea1 100644 --- a/lib/options/templates/options_panel.html +++ b/lib/options/templates/options_panel.html @@ -274,4 +274,4 @@
- + diff --git a/tests/test_hedge_plan_history_stats.py b/tests/test_hedge_plan_history_stats.py index 1d17f68..ea451b0 100644 --- a/tests/test_hedge_plan_history_stats.py +++ b/tests/test_hedge_plan_history_stats.py @@ -4,6 +4,7 @@ import unittest from lib.hedge_plan.hedge_plan_db import ( _metrics_from_pnls, + active_options_targets_by_inst, delete_plan, init_hedge_plan_tables, insert_leg, @@ -116,6 +117,44 @@ class TestHedgeHistoryStats(unittest.TestCase): self.assertIn("永续 ETH/USDT:USDT", s) self.assertIn("ETH-USD_UM-260715-1790-P", s) + def test_active_options_targets_are_read_only_plan_targets(self): + conn = _mem() + pid = insert_plan( + conn, + { + "plan_type": "options_options", + "status": "active", + "underlying": "ETH", + "target_price_up": 1950, + "target_price_down": 1800, + }, + ) + insert_leg( + conn, + { + "plan_id": pid, + "leg_role": "option_a", + "inst_id": "ETH-USD_UM-260719-1890-C", + "opt_type": "C", + "status": "open", + }, + ) + insert_leg( + conn, + { + "plan_id": pid, + "leg_role": "option_b", + "inst_id": "ETH-USD_UM-260719-1850-P", + "opt_type": "P", + "status": "open", + }, + ) + + targets = active_options_targets_by_inst(conn) + self.assertEqual(targets["ETH-USD_UM-260719-1890-C"]["target_index"], 1950) + 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") + if __name__ == "__main__": unittest.main()