diff --git a/lib/common/static/instance_theme.css b/lib/common/static/instance_theme.css
index 93749bc..0c85550 100644
--- a/lib/common/static/instance_theme.css
+++ b/lib/common/static/instance_theme.css
@@ -3727,6 +3727,11 @@ html[data-theme="light"] .options-estimate-row {
.opt-history-table td:nth-child(6) {
width: 20%;
}
+.opt-history-table th:nth-child(7),
+.opt-history-table td:nth-child(7) {
+ width: 8%;
+ text-align: center;
+}
.opt-hist-time {
font-size: 0.64rem;
white-space: nowrap;
diff --git a/lib/common/static/options_panel.js b/lib/common/static/options_panel.js
index d69c532..c12b3ad 100644
--- a/lib/common/static/options_panel.js
+++ b/lib/common/static/options_panel.js
@@ -215,7 +215,7 @@
function fmtUsdc(v) {
if (v === null || v === undefined || Number.isNaN(Number(v))) return "—";
- return Number(v).toFixed(4).replace(/\.?0+$/, "") || "0";
+ return Number(v).toFixed(2);
}
function fmtClosePreview(preview, premiumPaid) {
@@ -525,7 +525,7 @@
const closePreview = p.close_preview || {};
const closeSheets = p.avail_pos != null && Number(p.avail_pos) > 0 ? p.avail_pos : p.pos;
const tickSz = p.tick_sz;
- const premTxt = fmtDisplay(p.premium_paid_fmt, p.premium_paid != null ? fmt(p.premium_paid, 4).replace(/\.?0+$/, "") : null);
+ const premTxt = fmtDisplay(p.premium_paid_fmt, p.premium_paid != null ? fmtUsdc(p.premium_paid) : null);
const avgTxt = fmtDisplay(p.avg_px_fmt, fmtOptionPx(p.avg_px, tickSz));
const markTxt = fmtDisplay(p.mark_px_fmt, fmtOptionPx(p.mark_px, tickSz));
return (
@@ -899,6 +899,19 @@
}
}
+ async function deleteHistoryRow(key, status) {
+ const warn = status === "open"
+ ? "该记录仍为持仓中,仅从列表隐藏,不影响交易所持仓.确认删除?"
+ : "确认从列表隐藏该条历史记录?";
+ if (!confirm(warn)) return;
+ const r = await apiJson("/api/options/history/" + encodeURIComponent(key), { method: "DELETE" });
+ if (!r.ok) {
+ alert(r.msg || "删除失败");
+ return;
+ }
+ refreshAllPositions();
+ }
+
function optHistoryStatus(h) {
if (h.status_label) return h.status_label;
if (h.status === "open") return "持仓中";
@@ -921,26 +934,33 @@
tbody.innerHTML = "";
const list = (d.ok && d.history) || [];
if (!list.length) {
- tbody.innerHTML = '
| 暂无历史记录 |
';
+ tbody.innerHTML = '| 暂无历史记录 |
';
return;
}
list.forEach(function (h) {
const tr = document.createElement("tr");
- const premTxt = fmtDisplay(h.premium_paid_fmt, h.premium_paid != null ? fmt(h.premium_paid, 2) : null);
+ const premTxt = fmtDisplay(h.premium_paid_fmt, h.premium_paid != null ? fmtUsdc(h.premium_paid) : null);
const isOpen = h.status === "open";
const pnl = isOpen ? null : h.realized_pnl;
const pnlTxt = pnl != null ? fmt(pnl, 2) : "—";
const pnlCls = pnl > 0 ? "pos-pnl-profit" : pnl < 0 ? "pos-pnl-loss" : "";
const timeTxt = (h.closed_at || h.created_at || "—").replace("T", " ").slice(0, 19);
+ const histKey = h.history_key || "";
tr.innerHTML =
'' + (h.inst_id || "") + " | " +
"" + fmt(h.sheets, 0) + " | " +
"" + premTxt + " | " +
"" + optHistoryStatusHtml(h) + " | " +
'' + pnlTxt + " | " +
- '' + timeTxt + " | ";
+ '' + timeTxt + " | " +
+ ' | ';
tbody.appendChild(tr);
});
+ tbody.querySelectorAll(".opt-history-del").forEach(function (btn) {
+ btn.addEventListener("click", function () {
+ deleteHistoryRow(btn.getAttribute("data-key"), btn.getAttribute("data-status"));
+ });
+ });
}
function refreshAllPositions() {
diff --git a/lib/exchange/okx_options_lib.py b/lib/exchange/okx_options_lib.py
index d615ee3..8efc2f8 100644
--- a/lib/exchange/okx_options_lib.py
+++ b/lib/exchange/okx_options_lib.py
@@ -117,10 +117,32 @@ def format_option_px(px: float, tick_sz: Any) -> str:
def format_usdc_amount(v: float | None) -> str | None:
- """USDC 金额展示(与交易所持仓/历史一致,最多 4 位小数)."""
+ """USDC 金额展示(权利金/回收等,固定 2 位小数)."""
if v is None:
return None
- return f"{float(v):.4f}".rstrip("0").rstrip(".") or "0"
+ return f"{float(v):.2f}"
+
+
+def is_option_full_close_history(raw: dict[str, Any]) -> bool:
+ """仅保留 OKX 历史仓位中的「全部平仓/强平/ADL 全平」记录,排除部分平仓."""
+ close_type = str(raw.get("type") or "").strip()
+ return close_type in ("2", "3", "6")
+
+
+def option_history_row_key(
+ *,
+ source: str,
+ inst_id: str = "",
+ pos_id: str | None = None,
+ close_ms: int | None = None,
+) -> str:
+ inst_id = (inst_id or "").strip()
+ pos_id = (pos_id or "").strip()
+ if source == "live":
+ return f"live:{inst_id}:{pos_id or close_ms or '0'}"
+ if pos_id:
+ return f"ex:{pos_id}"
+ return f"ex:{inst_id}:{close_ms or 0}"
def _ms_to_iso(ms: Any) -> str | None:
@@ -815,6 +837,7 @@ def fetch_all_option_positions_history(
if after is not None and str(oldest) == after:
break
after = str(oldest)
+ out = [r for r in out if is_option_full_close_history(r)]
out.sort(key=lambda r: int(_safe_float(r.get("uTime")) or 0), reverse=True)
return out[:cap]
@@ -854,9 +877,17 @@ def format_option_history_row(
status_label = "强平"
else:
status_label = "已平"
+ pos_id = str(raw.get("posId") or "").strip() or None
+ close_ms = int(utime) if utime is not None else None
return {
"source": "exchange",
- "pos_id": str(raw.get("posId") or "").strip() or None,
+ "history_key": option_history_row_key(
+ source="exchange",
+ inst_id=inst_id,
+ pos_id=pos_id,
+ close_ms=close_ms,
+ ),
+ "pos_id": pos_id,
"inst_id": inst_id,
"underlying": uly,
"opt_type": opt_type,
@@ -876,7 +907,7 @@ def format_option_history_row(
"close_type": close_type,
"created_at": _ms_to_iso(ctime),
"closed_at": _ms_to_iso(utime),
- "close_ms": int(utime) if utime is not None else None,
+ "close_ms": close_ms,
"tick_sz": tick_sz,
"raw": raw,
}
@@ -889,9 +920,17 @@ def format_live_option_history_row(
) -> dict[str, Any]:
"""将当前持仓格式化为历史列表中的「持仓中」行."""
inst_id = str(row.get("inst_id") or "").strip()
+ pos_id = str((row.get("raw") or {}).get("posId") or "").strip() or None
+ close_ms = open_ms
return {
"source": "live",
- "pos_id": str((row.get("raw") or {}).get("posId") or "").strip() or None,
+ "history_key": option_history_row_key(
+ source="live",
+ inst_id=inst_id,
+ pos_id=pos_id,
+ close_ms=close_ms,
+ ),
+ "pos_id": pos_id,
"inst_id": inst_id,
"underlying": str(row.get("underlying") or inst_id.split("-")[0] or ""),
"opt_type": row.get("opt_type"),
diff --git a/lib/options/options_db.py b/lib/options/options_db.py
index 64ec61e..2ec91f3 100644
--- a/lib/options/options_db.py
+++ b/lib/options/options_db.py
@@ -46,6 +46,14 @@ def init_options_tables(conn: sqlite3.Connection) -> None:
)
"""
)
+ conn.execute(
+ """
+ CREATE TABLE IF NOT EXISTS options_history_hidden (
+ history_key TEXT PRIMARY KEY,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+ )
+ """
+ )
conn.execute(
"""
CREATE TABLE IF NOT EXISTS options_transfer_log (
diff --git a/lib/options/options_register.py b/lib/options/options_register.py
index b526fa2..7528c7a 100644
--- a/lib/options/options_register.py
+++ b/lib/options/options_register.py
@@ -915,6 +915,17 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
finally:
conn.close()
+ hidden_keys: set[str] = set()
+ conn = cfg["get_db"]()
+ try:
+ init_options_tables(conn)
+ hidden_keys = {
+ str(r["history_key"])
+ for r in conn.execute("SELECT history_key FROM options_history_hidden").fetchall()
+ }
+ finally:
+ conn.close()
+
hist_raw = fetch_all_option_positions_history(ex, limit=200)
for raw in hist_raw:
inst_id = str(raw.get("instId") or "").strip()
@@ -925,7 +936,11 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
closed = [x for x in items if x.get("status") != "open"]
closed.sort(key=lambda x: int(x.get("close_ms") or 0), reverse=True)
open_rows.sort(key=lambda x: int(x.get("close_ms") or 0), reverse=True)
- history = open_rows + closed
+ history = [
+ x
+ for x in (open_rows + closed)
+ if str(x.get("history_key") or "") not in hidden_keys
+ ]
live_ids = {str(x.get("inst_id") or "") for x in open_rows}
return jsonify({"ok": True, "history": history, "live_inst_ids": sorted(live_ids)})
@@ -939,13 +954,26 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
return jsonify({"ok": True, **compute_options_stats(cfg["get_db"])})
- @app.route("/api/options/history/", methods=["DELETE"])
+ @app.route("/api/options/history/", methods=["DELETE"])
@lr
- def api_options_history_delete(trade_id: str):
+ def api_options_history_delete(history_key: str):
ex, err = _require_options_ex(cfg)
if ex is None:
return jsonify({"ok": False, "msg": err})
- return jsonify({"ok": False, "msg": "历史仓位来自交易所,不支持本地删除"})
+ key = (history_key or "").strip()
+ if not key:
+ return jsonify({"ok": False, "msg": "缺少 history_key"})
+ conn = cfg["get_db"]()
+ try:
+ init_options_tables(conn)
+ conn.execute(
+ "INSERT OR IGNORE INTO options_history_hidden (history_key) VALUES (?)",
+ (key,),
+ )
+ conn.commit()
+ finally:
+ conn.close()
+ return jsonify({"ok": True})
def _start_monitor_thread(app: Flask, cfg: dict[str, Any]) -> None:
diff --git a/lib/options/templates/options_panel.html b/lib/options/templates/options_panel.html
index cb898df..ff1d4f1 100644
--- a/lib/options/templates/options_panel.html
+++ b/lib/options/templates/options_panel.html
@@ -198,10 +198,11 @@
状态 |
盈亏 |
时间 |
+ 操作 |
- | 加载中… |
+ | 加载中… |
@@ -211,4 +212,4 @@
-
+
diff --git a/tests/test_options_sync.py b/tests/test_options_sync.py
index a38b300..358ebe5 100644
--- a/tests/test_options_sync.py
+++ b/tests/test_options_sync.py
@@ -4,6 +4,7 @@ import sqlite3
from lib.exchange.okx_options_lib import (
format_option_history_row,
format_usdc_amount,
+ is_option_full_close_history,
resolve_option_close_from_history,
)
from lib.options.options_db import init_options_tables
@@ -11,9 +12,16 @@ from lib.options.options_monitor_lib import sync_open_options_trades
def test_format_usdc_amount():
- assert format_usdc_amount(4.896) == "4.896"
- assert format_usdc_amount(4.90) == "4.9"
- assert format_usdc_amount(4.0) == "4"
+ assert format_usdc_amount(4.896) == "4.90"
+ assert format_usdc_amount(4.9) == "4.90"
+ assert format_usdc_amount(4.0) == "4.00"
+
+
+def test_is_option_full_close_history():
+ assert is_option_full_close_history({"type": "2"})
+ assert is_option_full_close_history({"type": "3"})
+ assert not is_option_full_close_history({"type": "1"})
+ assert not is_option_full_close_history({"type": "5"})
def test_format_option_history_row():
@@ -36,7 +44,8 @@ def test_format_option_history_row():
assert row["realized_pnl"] == -3.99
assert row["status_label"] == "已平"
assert row["open_avg_px_fmt"] == "380"
- assert row["premium_paid_fmt"] == "3.8"
+ assert row["premium_paid_fmt"] == "3.80"
+ assert row["history_key"] == "ex:pos-btc"
def test_resolve_option_close_from_history_picks_latest():