Show hedge targets in options monitoring

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-17 07:15:00 +08:00
parent 6e6cb7caac
commit 8f71dbe874
8 changed files with 150 additions and 10 deletions
+1 -1
View File
@@ -904,7 +904,7 @@
"\"><code>" +
contracts +
"</code></td><td>" +
(p.status || "") +
'<span class="hp-plan-active">进行中</span>' +
"</td><td>" +
activeTargetLabel(p) +
"</td><td>" +
+20
View File
@@ -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;
+40 -8
View File
@@ -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 (
'<div class="opt-target-row opt-target-row--managed">' +
'<span class="opt-target-row-label">对冲计划</span>' +
'<span class="opt-target-armed">计划 #' +
hedgeTarget.plan_id +
" · " +
side +
" " +
fmt(hedgeTarget.target_index, 1) +
"</span>" +
'<span class="muted opt-target-row-hint">进行中 · 由对冲计划监控,到位后仅平盈利腿</span>' +
"</div>"
);
}
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 (
'<div class="opt-target-mon-item">' +
'<div class="opt-target-mon-item' + (managed ? " opt-target-mon-item--managed" : "") + '">' +
'<code class="opt-target-mon-inst" title="' + (t.inst_id || "") + '">' + (t.inst_id || "") + "</code>" +
'<span class="opt-target-mon-rule">' + side + " " + fmt(t.target_index, 1) + "</span>" +
'<button type="button" class="btn-secondary opt-target-mon-cancel" data-inst="' + (t.inst_id || "") + '">取消</button>' +
(managed
? '<span class="opt-target-mon-managed">对冲计划 #' + (t.plan_id || "") + " · 进行中</span>"
: '<button type="button" class="btn-secondary opt-target-mon-cancel" data-inst="' + (t.inst_id || "") + '">取消</button>') +
"</div>"
);
}).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 {
+39
View File
@@ -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 == "":
+5
View File
@@ -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:
+5
View File
@@ -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()
+1 -1
View File
@@ -274,4 +274,4 @@
</div>
</div>
<script src="/static/options_expiry_countdown.js?v=1"></script>
<script src="/static/options_panel.js?v=37"></script>
<script src="/static/options_panel.js?v=38"></script>
+39
View File
@@ -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()