" +
rowsHtml +
@@ -73,36 +121,84 @@
return ' class="inst-dash-row" data-dash-tab="' + escapeHtml(tab || "") + '" role="link" tabindex="0"';
}
+ function mergeOrderLive(items, orderPrices) {
+ const map = {};
+ (orderPrices || []).forEach(function (p) {
+ if (p && p.id != null) map[String(p.id)] = p;
+ });
+ return (items || []).map(function (it) {
+ const live = map[String(it.id)] || {};
+ const mark =
+ live.exchange_mark_price != null
+ ? live.exchange_mark_price
+ : live.price != null
+ ? live.price
+ : it.mark_price;
+ const contracts =
+ live.contracts != null
+ ? live.contracts
+ : live.order_amount != null
+ ? live.order_amount
+ : it.contracts;
+ const entry =
+ live.avg_entry_price != null
+ ? live.avg_entry_price
+ : it.entry;
+ return Object.assign({}, it, {
+ entry: entry,
+ mark_price: mark,
+ mark_display: live.price_display || null,
+ contracts: contracts,
+ tp_profit: live.reward_at_tp_usdt != null ? live.reward_at_tp_usdt : it.tp_profit,
+ float_pnl: live.float_pnl != null ? live.float_pnl : it.float_pnl,
+ });
+ });
+ }
+
function renderOrdersTable(items) {
const rows = items
.map(function (it) {
+ const sym = it.symbol || "-";
+ const mark =
+ it.mark_display != null && it.mark_display !== ""
+ ? escapeHtml(it.mark_display)
+ : fmtNum(it.mark_price);
+ const tpProfit =
+ it.tp_profit != null && Number.isFinite(Number(it.tp_profit))
+ ? '' + Number(it.tp_profit).toFixed(2) + "U"
+ : "—";
return (
"" +
- "| " +
- escapeHtml(it.symbol || "-") +
- " | " +
- "" +
- escapeHtml(it.direction_label || it.direction || "-") +
- " | " +
- "" +
- escapeHtml(it.subtitle || "—") +
- " | " +
+ '' +
+ escapeHtml(sym) +
+ " | " +
+ dirCell(it) +
"" +
fmtNum(it.entry) +
" | " +
"" +
- fmtNum(it.stop_loss) +
+ mark +
" | " +
"" +
- fmtNum(it.take_profit) +
+ fmtNum(it.contracts) +
" | " +
+ "" +
+ tpProfit +
+ " | " +
+ "" +
+ fmtPnlPlain(it.float_pnl) +
+ " | " +
+ "— | " +
"
"
);
})
.join("");
- return tableWrap(["合约", "方向", "类型", "入场", "止损", "止盈"], rows);
+ return tableWrap(
+ ["合约", "方向", "开仓价", "标记价", "张数", "盈利金额", "浮盈", "操作"],
+ rows
+ );
}
function renderKeysTable(items) {
@@ -115,9 +211,7 @@
"" +
escapeHtml(it.symbol || "-") +
" | " +
- "" +
- escapeHtml(it.direction_label || it.direction || "-") +
- " | " +
+ dirCell(it) +
"" +
escapeHtml(it.subtitle || "—") +
" | " +
@@ -148,9 +242,7 @@
"" +
escapeHtml(it.symbol || "-") +
" | " +
- "" +
- escapeHtml(it.direction_label || it.direction || "-") +
- " | " +
+ dirCell(it) +
"" +
escapeHtml(it.status || "—") +
" | " +
@@ -167,12 +259,12 @@
function renderOptionsTable(items) {
const rows = items
.map(function (it) {
- const opt =
- String(it.opt_type || "").toUpperCase() === "C"
+ const opt = it.opt_type_label ||
+ (String(it.opt_type || "").toUpperCase() === "C"
? "Call"
: String(it.opt_type || "").toUpperCase() === "P"
? "Put"
- : it.opt_type || "—";
+ : it.opt_type || "—");
return (
"" +
"| " +
+ escapeHtml(it.source_label || "纯期权") +
+ " | " +
+ "" +
escapeHtml(opt) +
" | " +
"" +
fmtNum(it.pos) +
" | " +
"" +
+ fmtExpiry(it.exp_time_ms) +
+ " | " +
+ "" +
+ escapeHtml(it.target_monitor || "—") +
+ " | " +
+ "" +
fmtPnl(it.pnl) +
" | " +
"
"
);
})
.join("");
- return tableWrap(["合约", "类型", "张数", "盈亏"], rows);
+ return tableWrap(["合约", "来源", "类型", "张数", "到期时间", "目标监控", "盈亏"], rows);
}
function renderHedgeTable(items) {
const rows = items
.map(function (it) {
+ const stCls = it.status_active ? "inst-dash-status-active" : "";
+ const stText = it.status_label || (it.status_active ? "进行中" : it.status || "—");
return (
"" +
"| " +
- escapeHtml(it.plan_type || "—") +
+ escapeHtml(it.plan_type_label || it.plan_type || "—") +
+ " | " +
+ '' +
+ escapeHtml(stText) +
" | " +
"" +
- escapeHtml(it.status || "—") +
- " | " +
- "" +
- escapeHtml(it.subtitle || "—") +
+ escapeHtml(it.contracts_summary || it.subtitle || "—") +
" | " +
"
"
);
@@ -290,12 +395,28 @@
loading = true;
if (status && !(opts && opts.silent)) status.textContent = "加载中…";
try {
- const res = await fetch("/api/instance/dashboard", { credentials: "same-origin" });
- const data = await res.json().catch(function () {
+ const [dashRes, priceRes] = await Promise.all([
+ fetch("/api/instance/dashboard", { credentials: "same-origin" }),
+ fetch("/api/price_snapshot", { credentials: "same-origin" }).catch(function () {
+ return null;
+ }),
+ ]);
+ const data = await dashRes.json().catch(function () {
return {};
});
- if (!res.ok || !data.ok) {
- throw new Error(data.msg || res.statusText || "加载失败");
+ if (!dashRes.ok || !data.ok) {
+ throw new Error(data.msg || dashRes.statusText || "加载失败");
+ }
+ let orderPrices = [];
+ if (priceRes && priceRes.ok) {
+ try {
+ const snap = await priceRes.json();
+ orderPrices = snap.order_prices || [];
+ } catch (_) {}
+ }
+ if (data.orders && Array.isArray(data.orders.items)) {
+ data.orders.items = mergeOrderLive(data.orders.items, orderPrices);
+ data.orders.count = data.orders.items.length;
}
if (updated) updated.textContent = "更新 " + (data.updated_at || "—");
if (sections) {
@@ -305,6 +426,14 @@
sections.innerHTML =
html || '当前无活跃监控与持仓
';
bindClicks(sections);
+ if (global.OptionsExpiryCountdown) {
+ if (typeof global.OptionsExpiryCountdown.tick === "function") {
+ global.OptionsExpiryCountdown.tick(sections);
+ }
+ if (typeof global.OptionsExpiryCountdown.ensureTimer === "function") {
+ global.OptionsExpiryCountdown.ensureTimer();
+ }
+ }
}
if (status) status.textContent = "";
} catch (e) {
diff --git a/lib/common/static/instance_page.css b/lib/common/static/instance_page.css
index 00bca12..0b56f37 100644
--- a/lib/common/static/instance_page.css
+++ b/lib/common/static/instance_page.css
@@ -287,3 +287,8 @@
.inst-dash-table tbody tr:last-child td{border-bottom:none}
.inst-dash-table tbody tr.inst-dash-row{cursor:pointer}
.inst-dash-table tbody tr.inst-dash-row:hover{background:#1e2740}
+.inst-dash-sym-link{color:#8fc8ff;text-decoration:underline}
+.inst-dash-dir-long{color:#4cd97f;font-weight:600}
+.inst-dash-dir-short{color:#ff6666;font-weight:600}
+.inst-dash-status-active{color:#4cd97f;font-weight:600}
+.inst-dash-table .pos-tp-profit{color:#cfd3ef}
diff --git a/lib/hub/hub_bridge.py b/lib/hub/hub_bridge.py
index 9ec93d8..26fac86 100644
--- a/lib/hub/hub_bridge.py
+++ b/lib/hub/hub_bridge.py
@@ -73,6 +73,7 @@ def install_instance_theme_static(app) -> None:
"instance_live.js": "application/javascript; charset=utf-8",
"instance_settings_prefs.js": "application/javascript; charset=utf-8",
"instance_dashboard.js": "application/javascript; charset=utf-8",
+ "options_expiry_countdown.js": "application/javascript; charset=utf-8",
"options_panel.js": "application/javascript; charset=utf-8",
"order_entry_model.js": "application/javascript; charset=utf-8",
"focus_chart_page.js": "application/javascript; charset=utf-8",
diff --git a/lib/instance/instance_dashboard_lib.py b/lib/instance/instance_dashboard_lib.py
index 846c8b7..8f5b8a6 100644
--- a/lib/instance/instance_dashboard_lib.py
+++ b/lib/instance/instance_dashboard_lib.py
@@ -67,12 +67,137 @@ def _format_order_item(od: dict[str, Any]) -> dict[str, Any]:
"direction": direction,
"direction_label": _dir_label(direction),
"entry": entry,
+ "mark_price": None,
+ "contracts": _safe_float(od.get("order_amount")),
+ "tp_profit": None,
+ "float_pnl": None,
"stop_loss": sl,
"take_profit": tp,
"status": od.get("status") or "active",
}
+OPTIONS_SOURCE_LABELS = {
+ "option": "纯期权",
+ "perp_options": "永期对冲",
+ "options_options": "期期对冲",
+}
+
+HEDGE_ACTIVE_STATUSES = frozenset({"opening", "active", "partial"})
+
+
+def _resolve_options_source(conn, inst_id: str) -> tuple[str, str]:
+ """根据进行中对冲计划腿判定来源;默认纯期权."""
+ if not inst_id or not _table_exists(conn, "hedge_plans") or not _table_exists(conn, "hedge_plan_legs"):
+ return "option", OPTIONS_SOURCE_LABELS["option"]
+ try:
+ row = conn.execute(
+ """
+ SELECT p.plan_type
+ FROM hedge_plans p
+ JOIN hedge_plan_legs l ON l.plan_id = p.id
+ WHERE p.status IN ('opening', 'active', 'partial')
+ AND l.status = 'open'
+ AND l.inst_id = ?
+ ORDER BY p.id DESC
+ LIMIT 1
+ """,
+ (inst_id,),
+ ).fetchone()
+ except Exception:
+ return "option", OPTIONS_SOURCE_LABELS["option"]
+ if not row:
+ return "option", OPTIONS_SOURCE_LABELS["option"]
+ pt = str((_row_dict(row).get("plan_type") if isinstance(row, dict) else row[0]) or "").strip()
+ if pt in OPTIONS_SOURCE_LABELS:
+ return pt, OPTIONS_SOURCE_LABELS[pt]
+ return "option", OPTIONS_SOURCE_LABELS["option"]
+
+
+def _format_options_target(p: dict[str, Any]) -> str:
+ hedge = p.get("hedge_plan_target") if isinstance(p.get("hedge_plan_target"), dict) else None
+ opt_type = str(p.get("opt_type") or p.get("optType") or "").upper()
+ if hedge:
+ ot = str(hedge.get("opt_type") or opt_type).upper()
+ side = "Put ≤" if ot == "P" else "Call ≥"
+ tgt = _safe_float(hedge.get("target_index"))
+ pid = hedge.get("plan_id")
+ if tgt is not None:
+ return f"对冲#{pid} {side} {tgt:g}" if pid is not None else f"{side} {tgt:g}"
+ tgt = _safe_float(p.get("target_index"))
+ if tgt is not None and tgt > 0:
+ side = "Put ≤" if opt_type == "P" else "Call ≥"
+ return f"{side} {tgt:g}"
+ return "—"
+
+
+def _format_options_item(p: dict[str, Any], *, conn=None) -> dict[str, Any]:
+ inst = str(p.get("inst_id") or p.get("instId") or "-").strip() or "-"
+ opt_type = str(p.get("opt_type") or p.get("optType") or "").upper()
+ label = "Call" if opt_type == "C" else "Put" if opt_type == "P" else (opt_type or "OPT")
+ upl = _safe_float(p.get("upl"))
+ net = None
+ try:
+ from lib.options.options_positions_lib import net_pnl_from_display_row
+
+ net = net_pnl_from_display_row(p)
+ except Exception:
+ net = None
+ pnl = net if net is not None else upl
+ pos = _safe_float(p.get("pos"))
+ exp_ms = p.get("exp_time_ms")
+ if exp_ms is None:
+ exp_ms = p.get("exp_time")
+ try:
+ exp_ms = int(float(exp_ms)) if exp_ms not in (None, "") else None
+ except (TypeError, ValueError):
+ exp_ms = None
+ source_key, source_label = (
+ _resolve_options_source(conn, inst) if conn is not None else ("option", OPTIONS_SOURCE_LABELS["option"])
+ )
+ return {
+ "id": inst,
+ "kind": "options",
+ "tab": "options",
+ "title": f"{inst} {label}",
+ "subtitle": f"张数 {pos if pos is not None else '-'}",
+ "inst_id": inst,
+ "opt_type": opt_type,
+ "opt_type_label": label,
+ "source": source_key,
+ "source_label": source_label,
+ "pos": pos,
+ "exp_time_ms": exp_ms,
+ "target_monitor": _format_options_target(p),
+ "pnl": round(pnl, 4) if pnl is not None else None,
+ }
+
+
+def _format_hedge_item(plan: dict[str, Any]) -> dict[str, Any]:
+ pid = plan.get("id")
+ underlying = plan.get("underlying") or "-"
+ plan_type = plan.get("plan_type") or ""
+ status = str(plan.get("status") or "")
+ summary = plan.get("contracts_summary") or ""
+ plan_type_label = OPTIONS_SOURCE_LABELS.get(plan_type, plan_type)
+ active = status in HEDGE_ACTIVE_STATUSES
+ status_label = "进行中" if active else (status or "—")
+ return {
+ "id": pid,
+ "kind": "hedge_plan",
+ "tab": "hedge_plan",
+ "title": f"对冲 #{pid} {underlying}",
+ "subtitle": " · ".join(x for x in (plan_type_label, status_label, summary) if x),
+ "underlying": underlying,
+ "plan_type": plan_type,
+ "plan_type_label": plan_type_label,
+ "status": status,
+ "status_label": status_label,
+ "status_active": active,
+ "contracts_summary": summary,
+ }
+
+
def _format_key_item(kd: dict[str, Any]) -> dict[str, Any]:
sym = kd.get("exchange_symbol") or kd.get("symbol") or "-"
direction = str(kd.get("direction") or "long").lower()
@@ -137,51 +262,6 @@ def _format_roll_item(rd: dict[str, Any]) -> dict[str, Any]:
}
-def _format_options_item(p: dict[str, Any]) -> dict[str, Any]:
- inst = p.get("inst_id") or p.get("instId") or "-"
- opt_type = str(p.get("opt_type") or p.get("optType") or "").upper()
- label = "Call" if opt_type == "C" else "Put" if opt_type == "P" else (opt_type or "OPT")
- upl = _safe_float(p.get("upl"))
- net = None
- try:
- from lib.options.options_positions_lib import net_pnl_from_display_row
-
- net = net_pnl_from_display_row(p)
- except Exception:
- net = None
- pnl = net if net is not None else upl
- pos = _safe_float(p.get("pos"))
- return {
- "id": inst,
- "kind": "options",
- "tab": "options",
- "title": f"{inst} {label}",
- "subtitle": f"张数 {pos if pos is not None else '-'}",
- "inst_id": inst,
- "opt_type": opt_type,
- "pnl": round(pnl, 4) if pnl is not None else None,
- "pos": pos,
- }
-
-
-def _format_hedge_item(plan: dict[str, Any]) -> dict[str, Any]:
- pid = plan.get("id")
- underlying = plan.get("underlying") or "-"
- plan_type = plan.get("plan_type") or ""
- status = plan.get("status") or ""
- summary = plan.get("contracts_summary") or ""
- return {
- "id": pid,
- "kind": "hedge_plan",
- "tab": "hedge_plan",
- "title": f"对冲 #{pid} {underlying}",
- "subtitle": " · ".join(x for x in (plan_type, status, summary) if x),
- "underlying": underlying,
- "plan_type": plan_type,
- "status": status,
- }
-
-
def _table_exists(conn, name: str) -> bool:
try:
row = conn.execute(
@@ -253,6 +333,8 @@ def collect_hedge_plans(conn) -> list[dict[str, Any]]:
def collect_options_items(
fetch_options_positions: Optional[Callable[[], list[dict[str, Any]]]] = None,
+ *,
+ conn=None,
) -> list[dict[str, Any]]:
if not callable(fetch_options_positions):
return []
@@ -264,7 +346,7 @@ def collect_options_items(
for p in raw:
if not isinstance(p, dict):
continue
- out.append(_format_options_item(p))
+ out.append(_format_options_item(p, conn=conn))
return out
@@ -279,7 +361,7 @@ def build_instance_dashboard_payload(
trends = collect_trends(conn)
rolls = collect_rolls(conn)
strategy_items = trends + rolls
- options_items = collect_options_items(fetch_options_positions)
+ options_items = collect_options_items(fetch_options_positions, conn=conn)
hedge_items = collect_hedge_plans(conn) if hedge_enabled else []
now = datetime.now(timezone.utc).astimezone().strftime("%Y-%m-%d %H:%M:%S")
return {
diff --git a/lib/instance/templates/embed_shell.html b/lib/instance/templates/embed_shell.html
index 4e63144..4ea40b5 100644
--- a/lib/instance/templates/embed_shell.html
+++ b/lib/instance/templates/embed_shell.html
@@ -6,7 +6,7 @@
-
+
@@ -110,7 +110,8 @@ const ORDER_ENTRY_MODEL_CODE_TO_CATEGORY = {{ entry_model_code_to_category | toj
{% include 'embed_boot_scripts.html' %}
-
+
+
diff --git a/lib/instance/templates/index.html b/lib/instance/templates/index.html
index 804e1bb..825fb18 100644
--- a/lib/instance/templates/index.html
+++ b/lib/instance/templates/index.html
@@ -16,7 +16,7 @@
{{ pwa_app_name }}
-
+
@@ -1992,7 +1992,8 @@ tickOrderHoldDurations();
setInterval(refreshPriceSnapshotConditional, {{ price_refresh_seconds * 1000 }});
-
+
+