Show hedge targets in options monitoring
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -904,7 +904,7 @@
|
|||||||
"\"><code>" +
|
"\"><code>" +
|
||||||
contracts +
|
contracts +
|
||||||
"</code></td><td>" +
|
"</code></td><td>" +
|
||||||
(p.status || "") +
|
'<span class="hp-plan-active">进行中</span>' +
|
||||||
"</td><td>" +
|
"</td><td>" +
|
||||||
activeTargetLabel(p) +
|
activeTargetLabel(p) +
|
||||||
"</td><td>" +
|
"</td><td>" +
|
||||||
|
|||||||
@@ -3238,6 +3238,10 @@ html[data-theme="light"] .opt-be-dist-down {
|
|||||||
.hedge-plan-page-wrap .hp-pnl-pos {
|
.hedge-plan-page-wrap .hp-pnl-pos {
|
||||||
color: #7ee787;
|
color: #7ee787;
|
||||||
}
|
}
|
||||||
|
.hedge-plan-page-wrap .hp-plan-active {
|
||||||
|
color: #7ee787;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
.hedge-plan-page-wrap .hp-pnl-neg {
|
.hedge-plan-page-wrap .hp-pnl-neg {
|
||||||
color: #ff8a8a;
|
color: #ff8a8a;
|
||||||
}
|
}
|
||||||
@@ -4255,6 +4259,15 @@ html[data-theme="light"] .options-stats-pnl-summary .options-stat-item {
|
|||||||
color: #9ad0ff;
|
color: #9ad0ff;
|
||||||
font-variant-numeric: tabular-nums;
|
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 {
|
.options-page-wrap .opt-target-est {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -4320,6 +4333,13 @@ html[data-theme="light"] .options-stats-pnl-summary .options-stat-item {
|
|||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
font-size: 0.7rem;
|
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 {
|
.options-page-wrap .opt-bid-plain {
|
||||||
color: #dbe6ff;
|
color: #dbe6ff;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
|
|||||||
@@ -1217,6 +1217,23 @@
|
|||||||
|
|
||||||
function renderTargetDelegateRow(p) {
|
function renderTargetDelegateRow(p) {
|
||||||
const inst = p.inst_id || "";
|
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 tgt = p.target_index != null && p.target_index !== "" ? Number(p.target_index) : null;
|
||||||
const armed = tgt != null && Number.isFinite(tgt) && tgt > 0;
|
const armed = tgt != null && Number.isFinite(tgt) && tgt > 0;
|
||||||
const ethAmt = posEthAmount(p);
|
const ethAmt = posEthAmount(p);
|
||||||
@@ -1458,11 +1475,14 @@
|
|||||||
box.hidden = false;
|
box.hidden = false;
|
||||||
host.innerHTML = rows.map(function (t) {
|
host.innerHTML = rows.map(function (t) {
|
||||||
const side = (t.opt_type || "").toUpperCase() === "P" ? "Put ≤" : "Call ≥";
|
const side = (t.opt_type || "").toUpperCase() === "P" ? "Put ≤" : "Call ≥";
|
||||||
|
const managed = t.managed_by === "hedge_plan";
|
||||||
return (
|
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>" +
|
'<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>" +
|
'<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>"
|
"</div>"
|
||||||
);
|
);
|
||||||
}).join("");
|
}).join("");
|
||||||
@@ -1634,16 +1654,28 @@
|
|||||||
if (seq !== positionsRefreshSeq) return;
|
if (seq !== positionsRefreshSeq) return;
|
||||||
const list = resolvePositionsList(d);
|
const list = resolvePositionsList(d);
|
||||||
paintPositions(list);
|
paintPositions(list);
|
||||||
const fromPos = list
|
const fromPos = list.reduce(function (targets, p) {
|
||||||
.filter(function (p) { return p && p.target_index != null; })
|
if (!p) return targets;
|
||||||
.map(function (p) {
|
if (p.target_index != null) {
|
||||||
return {
|
targets.push({
|
||||||
id: p.target_monitor_id,
|
id: p.target_monitor_id,
|
||||||
inst_id: p.inst_id,
|
inst_id: p.inst_id,
|
||||||
opt_type: p.opt_type,
|
opt_type: p.opt_type,
|
||||||
target_index: p.target_index,
|
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) {
|
if (fromPos.length) {
|
||||||
paintTargetMonitors(fromPos);
|
paintTargetMonitors(fromPos);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -205,6 +205,45 @@ def attach_legs_to_plans(conn: sqlite3.Connection, plans: list[dict[str, Any]])
|
|||||||
return out
|
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]:
|
def _sf(v: Any) -> Optional[float]:
|
||||||
try:
|
try:
|
||||||
if v is None or v == "":
|
if v is None or v == "":
|
||||||
|
|||||||
@@ -34,16 +34,21 @@ def build_options_hub_snapshot(cfg: dict[str, Any]) -> dict[str, Any]:
|
|||||||
try:
|
try:
|
||||||
conn = cfg["get_db"]()
|
conn = cfg["get_db"]()
|
||||||
try:
|
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
|
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)
|
target_monitors = list_active_targets(conn) + list_closing_targets(conn)
|
||||||
tgt_map = targets_by_inst(conn)
|
tgt_map = targets_by_inst(conn)
|
||||||
|
hedge_target_map = active_options_targets_by_inst(conn)
|
||||||
for p in positions:
|
for p in positions:
|
||||||
mon = tgt_map.get(str(p.get("inst_id") or ""))
|
mon = tgt_map.get(str(p.get("inst_id") or ""))
|
||||||
if mon:
|
if mon:
|
||||||
p["target_index"] = mon.get("target_index")
|
p["target_index"] = mon.get("target_index")
|
||||||
p["target_monitor_id"] = mon.get("id")
|
p["target_monitor_id"] = mon.get("id")
|
||||||
p["target_monitor"] = mon
|
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:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -739,8 +739,10 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
conn = cfg["get_db"]()
|
conn = cfg["get_db"]()
|
||||||
try:
|
try:
|
||||||
from lib.options.options_target_lib import targets_by_inst
|
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)
|
tgt_map = targets_by_inst(conn)
|
||||||
|
hedge_target_map = active_options_targets_by_inst(conn)
|
||||||
rows = []
|
rows = []
|
||||||
for p in raw:
|
for p in raw:
|
||||||
inst = str(p.get("instId") or "").strip()
|
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_index"] = mon.get("target_index")
|
||||||
row["target_monitor_id"] = mon.get("id")
|
row["target_monitor_id"] = mon.get("id")
|
||||||
row["target_monitor"] = mon
|
row["target_monitor"] = mon
|
||||||
|
hedge_target = hedge_target_map.get(inst)
|
||||||
|
if hedge_target:
|
||||||
|
row["hedge_plan_target"] = hedge_target
|
||||||
rows.append(row)
|
rows.append(row)
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|||||||
@@ -274,4 +274,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/static/options_expiry_countdown.js?v=1"></script>
|
<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 (
|
from lib.hedge_plan.hedge_plan_db import (
|
||||||
_metrics_from_pnls,
|
_metrics_from_pnls,
|
||||||
|
active_options_targets_by_inst,
|
||||||
delete_plan,
|
delete_plan,
|
||||||
init_hedge_plan_tables,
|
init_hedge_plan_tables,
|
||||||
insert_leg,
|
insert_leg,
|
||||||
@@ -116,6 +117,44 @@ class TestHedgeHistoryStats(unittest.TestCase):
|
|||||||
self.assertIn("永续 ETH/USDT:USDT", s)
|
self.assertIn("永续 ETH/USDT:USDT", s)
|
||||||
self.assertIn("ETH-USD_UM-260715-1790-P", 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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user