diff --git a/lib/common/static/options_panel.js b/lib/common/static/options_panel.js
index 87d9e23..6f22a04 100644
--- a/lib/common/static/options_panel.js
+++ b/lib/common/static/options_panel.js
@@ -1500,6 +1500,13 @@
);
}
+ function formatProfitExitMultLabel(mult) {
+ const n = Number(mult);
+ if (!Number.isFinite(n) || n <= 0) return "1倍";
+ if (Math.abs(n - Math.round(n)) < 1e-9) return String(Math.round(n)) + "倍";
+ return fmt(n, 2) + "倍";
+ }
+
function renderProfitExitRow(p) {
const inst = p.inst_id || "";
if (p.hedge_plan_target) {
@@ -1511,10 +1518,9 @@
: 1;
const state = String(p.profit_exit_state || (enabled ? "active" : "idle"));
const req = p.profit_exit_required_recycle;
- let statusTxt = enabled
- ? ("监控中 · " + fmt(mult, 2) + "倍")
- : "未开启";
- if (enabled && state === "closing") statusTxt = "平仓挂单中 · " + fmt(mult, 2) + "倍";
+ const multLabel = formatProfitExitMultLabel(mult);
+ let statusTxt = enabled ? ("监控中 · " + multLabel) : "未开启";
+ if (enabled && state === "closing") statusTxt = "平仓挂单中 · " + multLabel;
return (
'
' +
'翻倍' +
@@ -1523,7 +1529,8 @@
'" +
'' +
+ inst + '" data-mode="' + (enabled ? "cancel" : "apply") + '">' +
+ (enabled ? "取消" : "应用") + "" +
'' + statusTxt + "" +
'' +
(enabled
@@ -1853,7 +1860,10 @@
const row = card ? card.querySelector(".opt-profit-exit-pos-row") : null;
const enabledEl = row ? row.querySelector(".opt-pos-profit-exit-enabled") : null;
const multEl = row ? row.querySelector(".opt-pos-profit-exit-mult") : null;
- const enabled = !!(enabledEl && enabledEl.checked);
+ const mode = btn && btn.getAttribute("data-mode");
+ let enabled = !!(enabledEl && enabledEl.checked);
+ if (mode === "cancel") enabled = false;
+ if (mode === "apply") enabled = true;
let mult = 1;
if (enabled) {
mult = parseFloat(multEl ? multEl.value : "1");
diff --git a/lib/instance/instance_dashboard_lib.py b/lib/instance/instance_dashboard_lib.py
index 39c96c4..f4c4c29 100644
--- a/lib/instance/instance_dashboard_lib.py
+++ b/lib/instance/instance_dashboard_lib.py
@@ -121,20 +121,40 @@ def _resolve_options_source(conn, inst_id: str) -> tuple[str, str, int | None]:
return default
+def _format_profit_exit_mult(mult: Any) -> str:
+ try:
+ n = float(mult)
+ except (TypeError, ValueError):
+ return "1倍"
+ if n <= 0:
+ return "1倍"
+ if abs(n - round(n)) < 1e-9:
+ return f"{int(round(n))}倍"
+ return f"{n:g}倍"
+
+
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:
+ rr = _safe_float(hedge.get("profit_rr"))
+ pid = hedge.get("plan_id")
+ if rr is not None and rr > 0:
+ return f"对冲#{pid} 盈亏比 {rr:g}" if pid is not None else f"盈亏比 {rr:g}"
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}"
+ parts: list[str] = []
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}"
+ parts.append(f"{side} {tgt:g}")
+ if p.get("profit_exit_enabled"):
+ parts.append(_format_profit_exit_mult(p.get("profit_exit_mult")))
+ if parts:
+ return " · ".join(parts)
return "—"
@@ -350,11 +370,39 @@ def collect_options_items(
raw = fetch_options_positions() or []
except Exception:
return []
+ pe_map: dict[str, dict[str, Any]] = {}
+ tgt_map: dict[str, dict[str, Any]] = {}
+ hedge_map: dict[str, dict[str, Any]] = {}
+ if conn is not None:
+ try:
+ from lib.options.options_profit_exit_lib import profit_exit_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
+
+ pe_map = profit_exit_by_inst(conn)
+ tgt_map = targets_by_inst(conn)
+ hedge_map = active_options_targets_by_inst(conn)
+ except Exception:
+ pe_map, tgt_map, hedge_map = {}, {}, {}
out: list[dict[str, Any]] = []
for p in raw:
if not isinstance(p, dict):
continue
- out.append(_format_options_item(p, conn=conn))
+ row = dict(p)
+ inst = str(row.get("inst_id") or row.get("instId") or "").strip()
+ mon = tgt_map.get(inst)
+ if mon:
+ row["target_index"] = mon.get("target_index")
+ pe = pe_map.get(inst)
+ if pe:
+ row["profit_exit_enabled"] = pe.get("profit_exit_enabled")
+ row["profit_exit_mult"] = pe.get("profit_exit_mult")
+ hedge = hedge_map.get(inst)
+ if hedge:
+ row["hedge_plan_target"] = hedge
+ if not mon:
+ row["target_index"] = hedge.get("target_index")
+ out.append(_format_options_item(row, conn=conn))
return out
diff --git a/lib/options/options_hub_lib.py b/lib/options/options_hub_lib.py
index cbe0d9b..76b4406 100644
--- a/lib/options/options_hub_lib.py
+++ b/lib/options/options_hub_lib.py
@@ -28,18 +28,36 @@ def build_options_hub_snapshot(cfg: dict[str, Any]) -> dict[str, Any]:
conn = cfg["get_db"]()
try:
from lib.hedge_plan.hedge_plan_db import active_options_targets_by_inst
+ from lib.options.options_profit_exit_lib import profit_exit_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)
+ profit_exit_map = profit_exit_by_inst(conn)
target_monitors.extend(hedge_target_map.values())
+ for pe in profit_exit_map.values():
+ if pe.get("profit_exit_enabled"):
+ target_monitors.append(
+ {
+ "inst_id": pe.get("inst_id"),
+ "exit_mode": "profit_exit",
+ "profit_exit_mult": pe.get("profit_exit_mult"),
+ "profit_exit_enabled": True,
+ }
+ )
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
+ pe = profit_exit_map.get(str(p.get("inst_id") or ""))
+ if pe:
+ p["profit_exit_enabled"] = pe.get("profit_exit_enabled")
+ p["profit_exit_mult"] = pe.get("profit_exit_mult")
+ p["profit_exit_state"] = pe.get("profit_exit_state")
+ p["profit_exit_required_recycle"] = pe.get("required_recycle")
hedge_target = hedge_target_map.get(str(p.get("inst_id") or ""))
if hedge_target:
p["hedge_plan_target"] = hedge_target
diff --git a/lib/options/templates/options_panel.html b/lib/options/templates/options_panel.html
index a1014c5..fffcd1d 100644
--- a/lib/options/templates/options_panel.html
+++ b/lib/options/templates/options_panel.html
@@ -350,4 +350,4 @@
-
+
diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js
index 62db67e..3374237 100644
--- a/manual_trading_hub/static/app.js
+++ b/manual_trading_hub/static/app.js
@@ -3928,9 +3928,15 @@
);
}
- function renderOptionsTargetCell(target) {
- if (!target) return "— | ";
- if (target.managed_by === "hedge_plan") {
+ function formatProfitExitMultLabel(mult) {
+ const n = Number(mult);
+ if (!Number.isFinite(n) || n <= 0) return "1倍";
+ if (Math.abs(n - Math.round(n)) < 1e-9) return String(Math.round(n)) + "倍";
+ return fmt(n, 2) + "倍";
+ }
+
+ function renderOptionsTargetCell(target, pos) {
+ if (target && target.managed_by === "hedge_plan") {
const rr = target.profit_rr != null ? Number(target.profit_rr) : null;
if (rr != null && rr > 0) {
return `对冲#${esc(target.plan_id)} 盈亏比 ${esc(fmt(rr, 2))} | `;
@@ -3939,9 +3945,30 @@
const px = target.target_index != null ? fmt(target.target_index, 1) : "—";
return `对冲#${esc(target.plan_id)} ${esc(side)} ${esc(px)} | `;
}
- const side = String(target.opt_type || "").toUpperCase() === "P" ? "Put≤" : "Call≥";
- const px = target.target_index != null ? fmt(target.target_index, 1) : "—";
- return `${esc(side)} ${esc(px)} | `;
+ const parts = [];
+ const hasIndex =
+ target &&
+ target.exit_mode !== "profit_exit" &&
+ target.target_index != null &&
+ Number(target.target_index) > 0;
+ if (hasIndex) {
+ const side = String(target.opt_type || (pos && pos.opt_type) || "").toUpperCase() === "P" ? "Put≤" : "Call≥";
+ parts.push(side + " " + fmt(target.target_index, 1));
+ }
+ const peOn =
+ !!(pos && pos.profit_exit_enabled) ||
+ !!(target && (target.exit_mode === "profit_exit" || target.profit_exit_enabled));
+ if (peOn) {
+ const mult =
+ pos && pos.profit_exit_mult != null
+ ? pos.profit_exit_mult
+ : target && target.profit_exit_mult != null
+ ? target.profit_exit_mult
+ : 1;
+ parts.push(formatProfitExitMultLabel(mult));
+ }
+ if (!parts.length) return "— | ";
+ return `${esc(parts.join(" · "))} | `;
}
function renderOptionsPositionsTable(pos, targets) {
@@ -3970,7 +3997,7 @@
${esc(optType)} |
${esc(p.pos)} |
${optionsExpiryCdHtml(p.exp_time_ms != null ? p.exp_time_ms : p.exp_time)} |
- ${renderOptionsTargetCell(target)}`;
+ ${renderOptionsTargetCell(target, p)}`;
if (showPnl) {
html += `${net == null ? "—" : fmt(net, 2)} |
${roi == null ? "—" : esc(Number(roi).toFixed(2)) + "%"} | `;
diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html
index 2e5808b..c6c381d 100644
--- a/manual_trading_hub/static/index.html
+++ b/manual_trading_hub/static/index.html
@@ -1767,6 +1767,6 @@
-
+