Show hedge targets in options monitoring
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -904,7 +904,7 @@
|
||||
"\"><code>" +
|
||||
contracts +
|
||||
"</code></td><td>" +
|
||||
(p.status || "") +
|
||||
'<span class="hp-plan-active">进行中</span>' +
|
||||
"</td><td>" +
|
||||
activeTargetLabel(p) +
|
||||
"</td><td>" +
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 == "":
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user