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()
|