diff --git a/lib/common/static/instance_theme.css b/lib/common/static/instance_theme.css
index 8b97866..93749bc 100644
--- a/lib/common/static/instance_theme.css
+++ b/lib/common/static/instance_theme.css
@@ -3600,62 +3600,15 @@ html[data-theme="light"] .options-estimate-row {
.options-page-wrap .opt-pos-cell--depth {
grid-column: span 2;
}
-.options-page-wrap .opt-bid-depth {
- display: flex;
- flex-wrap: wrap;
- gap: 5px;
- align-items: center;
-}
-.options-page-wrap .opt-bid-level {
- display: inline-grid;
- grid-template-columns: auto auto;
- align-items: center;
- gap: 5px;
- min-height: 24px;
- padding: 3px 9px;
- border: 1px solid rgba(82, 101, 143, 0.55);
- border-radius: 999px;
- background: rgba(18, 24, 37, 0.72);
+.options-page-wrap .opt-bid-plain {
color: #dbe6ff;
- line-height: 1;
font-variant-numeric: tabular-nums;
+ line-height: 1.35;
+ white-space: normal;
}
-.options-page-wrap .opt-bid-level--best {
- border-color: rgba(93, 143, 255, 0.9);
- background: linear-gradient(135deg, rgba(47, 86, 170, 0.72), rgba(28, 39, 68, 0.84));
- box-shadow: inset 0 0 0 1px rgba(122, 164, 255, 0.1), 0 6px 14px rgba(24, 46, 92, 0.24);
-}
-.options-page-wrap .opt-bid-rank {
- color: #91a4cc;
- font-size: 0.64rem;
- font-weight: 600;
-}
-.options-page-wrap .opt-bid-level--best .opt-bid-rank {
- color: #c8d8ff;
-}
-.options-page-wrap .opt-bid-price {
- color: #ffffff;
+.options-page-wrap .opt-close-value {
font-weight: 700;
-}
-.options-page-wrap .opt-close-preview {
- display: flex;
- flex-direction: column;
- align-items: flex-start;
-}
-.options-page-wrap .opt-close-main {
- display: inline-flex;
- align-items: baseline;
- gap: 4px;
- color: #f2f6ff;
- font-size: 0.9rem;
- font-weight: 800;
font-variant-numeric: tabular-nums;
- line-height: 1.05;
-}
-.options-page-wrap .opt-close-main span {
- color: #9aa8c2;
- font-size: 0.62rem;
- font-weight: 600;
}
.options-page-wrap .opt-close-rule {
margin-top: 8px;
@@ -3774,11 +3727,6 @@ 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 d148f11..d69c532 100644
--- a/lib/common/static/options_panel.js
+++ b/lib/common/static/options_panel.js
@@ -30,6 +30,24 @@
return Number(v).toFixed(d == null ? 2 : d);
}
+ function fmtDisplay(v, fallback) {
+ if (v !== null && v !== undefined && String(v).trim() !== "") return String(v);
+ if (fallback !== undefined) return fmtDisplay(fallback);
+ return "—";
+ }
+
+ function fmtOptionPx(v, tickSz) {
+ if (v === null || v === undefined || Number.isNaN(Number(v))) return "—";
+ const n = Number(v);
+ const tick = Number(tickSz);
+ if (!tickSz || Number.isNaN(tick) || tick <= 0) return String(n);
+ let decimals = 0;
+ if (tick < 1) decimals = Math.max(0, -Math.round(Math.log10(tick)));
+ else if (String(tick).indexOf(".") >= 0) decimals = String(tick).split(".")[1].length;
+ let s = n.toFixed(decimals).replace(/\.?0+$/, "");
+ return s || "0";
+ }
+
async function apiJson(url, opts) {
const r = await fetch(url, Object.assign({ credentials: "same-origin" }, opts || {}));
return r.json();
@@ -185,28 +203,32 @@
return price + "/" + size;
}
- function fmtCloseLevels(preview) {
+ function fmtCloseLevels(preview, tickSz) {
const levels = ((preview && preview.levels) || []).slice(0, 5);
if (!levels.length) return "—";
- return '
' + levels.map(function (x, idx) {
+ return levels.map(function (x, idx) {
const levelNo = x.level != null ? x.level : idx + 1;
- const levelCls = idx === 0 ? " opt-bid-level--best" : "";
- return (
- '' +
- '买' + levelNo + "" +
- '' + fmt(x.px, 4) + "" +
- ""
- );
- }).join("") + "
";
+ const pxTxt = fmtOptionPx(x.px, tickSz);
+ return "买" + levelNo + " " + pxTxt;
+ }).join(" · ");
}
- function fmtClosePreview(preview) {
+ function fmtUsdc(v) {
+ if (v === null || v === undefined || Number.isNaN(Number(v))) return "—";
+ return Number(v).toFixed(4).replace(/\.?0+$/, "") || "0";
+ }
+
+ function fmtClosePreview(preview, premiumPaid) {
if (!preview || preview.total_received == null) return "—";
- return (
- '' +
'
' + (p.inst_id || "") + '' +
@@ -517,17 +543,17 @@
: "") +
"
" +
'
' +
- '
权利金' + fmt(p.premium_paid, 4) + " USDC
" +
- '
开仓均价' + fmt(p.avg_px, 4) + "
" +
- '
标记价' + fmt(p.mark_px, 4) + "
" +
+ '
权利金' + premTxt + " USDC
" +
+ '
开仓均价' + avgTxt + "
" +
+ '
标记价' + markTxt + "
" +
'
指数价' + fmt(p.idx_px, 0) + "
" +
- '
买盘深度' + fmtCloseLevels(closePreview) + "
" +
- '
按买盘收回' + fmtClosePreview(closePreview) + "
" +
'
到期平衡' + fmt(p.expiry_be_px, 0) + "
" +
'
平掉回本' + fmt(p.close_be_px, 0) + "
" +
'
浮盈亏' + fmt(p.upl, 2) + "
" +
'
收益率' +
(p.upl_ratio_pct != null ? fmt(p.upl_ratio_pct, 2) + "%" : "—") + "
" +
+ '
买盘深度' + fmtCloseLevels(closePreview, tickSz) + "
" +
+ '
按买盘回收' + fmtClosePreview(closePreview, p.premium_paid) + "
" +
"
"
);
}
@@ -873,25 +899,10 @@
}
}
- async function deleteHistoryRow(id, status) {
- const warn = status === "open"
- ? "该记录仍为持仓中,仅删除本地记录,不影响交易所持仓.确认删除?"
- : "确认删除该条期权历史记录?";
- if (!confirm(warn)) return;
- const r = await apiJson("/api/options/history/" + encodeURIComponent(id), { 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 "持仓中";
if (h.status !== "closed") return "持仓中";
- if ((h.signal_note || "").indexOf("到期结算") >= 0) return "到期";
- if (h.premium_received === 0 && h.realized_pnl != null && h.realized_pnl < 0 && !h.close_ord_id) {
- return "到期";
- }
return "已平";
}
@@ -899,7 +910,7 @@
const s = optHistoryStatus(h);
let cls = "opt-hist-status";
if (s === "已平") cls += " opt-hist-status--closed";
- else if (s === "到期") cls += " opt-hist-status--expired";
+ else if (s === "到期" || s === "强平") cls += " opt-hist-status--expired";
else cls += " opt-hist-status--open";
return '
' + s + "";
}
@@ -910,13 +921,14 @@
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 = h.premium_paid != null ? fmt(h.premium_paid, 2) : "—";
- const pnl = h.realized_pnl;
+ const premTxt = fmtDisplay(h.premium_paid_fmt, h.premium_paid != null ? fmt(h.premium_paid, 2) : 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);
@@ -926,15 +938,9 @@
"
" + 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-id"), btn.getAttribute("data-status"));
- });
- });
}
function refreshAllPositions() {
diff --git a/lib/exchange/okx_options_lib.py b/lib/exchange/okx_options_lib.py
index 6cc01b0..d615ee3 100644
--- a/lib/exchange/okx_options_lib.py
+++ b/lib/exchange/okx_options_lib.py
@@ -116,6 +116,53 @@ def format_option_px(px: float, tick_sz: Any) -> str:
return f"{px:.{decimals}f}".rstrip("0").rstrip(".") or "0"
+def format_usdc_amount(v: float | None) -> str | None:
+ """USDC 金额展示(与交易所持仓/历史一致,最多 4 位小数)."""
+ if v is None:
+ return None
+ return f"{float(v):.4f}".rstrip("0").rstrip(".") or "0"
+
+
+def _ms_to_iso(ms: Any) -> str | None:
+ val = _safe_float(ms)
+ if val is None or val <= 0:
+ return None
+ try:
+ from datetime import datetime, timezone
+
+ dt = datetime.fromtimestamp(int(val) / 1000.0, tz=timezone.utc).astimezone()
+ return dt.strftime("%Y-%m-%d %H:%M:%S")
+ except (TypeError, ValueError, OSError):
+ return None
+
+
+def option_instrument_meta_cached(
+ ex: ccxt.okx,
+ inst_id: str,
+ cache: dict[str, dict[str, Any] | None] | None = None,
+) -> dict[str, Any] | None:
+ inst_id = (inst_id or "").strip()
+ if not inst_id:
+ return None
+ if cache is not None and inst_id in cache:
+ return cache[inst_id]
+ meta = fetch_option_instrument_meta(ex, inst_id)
+ if cache is not None:
+ cache[inst_id] = meta
+ return meta
+
+
+def tick_sz_and_ct_mult(
+ ex: ccxt.okx,
+ inst_id: str,
+ cache: dict[str, dict[str, Any] | None] | None = None,
+) -> tuple[Any, float]:
+ meta = option_instrument_meta_cached(ex, inst_id, cache)
+ tick_sz = meta.get("tickSz") if meta else None
+ ct_mult = _safe_float(meta.get("ctMult")) if meta else None
+ return tick_sz, ct_mult or 0.01
+
+
def _intrinsic_px_per_unit(opt_type: str, strike: float, index_px: float) -> float | None:
o = (opt_type or "").upper()
if o == "C" and index_px > strike:
@@ -732,6 +779,144 @@ def fetch_option_position_history(
return []
+def fetch_all_option_positions_history(
+ ex: ccxt.okx,
+ *,
+ limit: int = 200,
+) -> list[dict[str, Any]]:
+ """拉取 OKX 期权全部历史仓位(分页,按平仓时间倒序)."""
+ cap = max(1, min(int(limit), 500))
+ out: list[dict[str, Any]] = []
+ after: str | None = None
+ while len(out) < cap:
+ page_limit = min(100, cap - len(out))
+ params: dict[str, Any] = {
+ "instType": "OPTION",
+ "limit": str(page_limit),
+ }
+ if after is not None:
+ params["after"] = after
+ try:
+ resp = ex.private_get_account_positions_history(params)
+ except Exception:
+ break
+ rows = (resp or {}).get("data") or []
+ batch = [r for r in rows if isinstance(r, dict)]
+ if not batch:
+ break
+ out.extend(batch)
+ if len(batch) < page_limit:
+ break
+ utimes = [_safe_float(r.get("uTime")) for r in batch]
+ utimes = [int(u) for u in utimes if u is not None and u > 0]
+ if not utimes:
+ break
+ oldest = min(utimes)
+ if after is not None and str(oldest) == after:
+ break
+ after = str(oldest)
+ out.sort(key=lambda r: int(_safe_float(r.get("uTime")) or 0), reverse=True)
+ return out[:cap]
+
+
+def format_option_history_row(
+ raw: dict[str, Any],
+ *,
+ tick_sz: Any = None,
+ ct_mult: float = 0.01,
+) -> dict[str, Any]:
+ """标准化 OKX positions-history 单条记录供前端展示."""
+ from lib.options.options_pricing_lib import total_premium
+
+ inst_id = str(raw.get("instId") or "").strip()
+ open_avg = _safe_float(raw.get("openAvgPx"))
+ close_avg = _safe_float(raw.get("closeAvgPx"))
+ sheets = _safe_float(raw.get("closeTotalPos"))
+ if sheets is None or sheets <= 0:
+ sheets = _safe_float(raw.get("openMaxPos"))
+ sheets_i = int(abs(sheets or 0))
+ eth_amount = round(abs(sheets or 0) * ct_mult, 8) if sheets else 0.0
+ premium_paid = (
+ round(total_premium(open_avg, eth_amount), 8)
+ if open_avg is not None and eth_amount > 0
+ else None
+ )
+ realized = _safe_float(raw.get("realizedPnl"))
+ if realized is None:
+ realized = _safe_float(raw.get("pnl"))
+ pnl_ratio = _safe_float(raw.get("pnlRatio"))
+ close_type = str(raw.get("type") or "").strip()
+ utime = _safe_float(raw.get("uTime"))
+ ctime = _safe_float(raw.get("cTime"))
+ opt_type, strike = option_fields_from_inst_id(inst_id)
+ uly = str(raw.get("uly") or inst_id.split("-")[0] or "").replace("-USD_UM", "").replace("-USD", "")
+ if close_type in ("3", "4"):
+ status_label = "强平"
+ else:
+ status_label = "已平"
+ return {
+ "source": "exchange",
+ "pos_id": str(raw.get("posId") or "").strip() or None,
+ "inst_id": inst_id,
+ "underlying": uly,
+ "opt_type": opt_type,
+ "strike": strike,
+ "sheets": sheets_i,
+ "eth_amount": eth_amount,
+ "open_avg_px": open_avg,
+ "open_avg_px_fmt": format_option_px(open_avg, tick_sz) if open_avg is not None else None,
+ "close_avg_px": close_avg,
+ "close_avg_px_fmt": format_option_px(close_avg, tick_sz) if close_avg is not None else None,
+ "premium_paid": premium_paid,
+ "premium_paid_fmt": format_usdc_amount(premium_paid),
+ "realized_pnl": realized,
+ "pnl_ratio_pct": round(pnl_ratio * 100, 2) if pnl_ratio is not None else None,
+ "status": "closed",
+ "status_label": status_label,
+ "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,
+ "tick_sz": tick_sz,
+ "raw": raw,
+ }
+
+
+def format_live_option_history_row(
+ row: dict[str, Any],
+ *,
+ open_ms: int | None = None,
+) -> dict[str, Any]:
+ """将当前持仓格式化为历史列表中的「持仓中」行."""
+ inst_id = str(row.get("inst_id") or "").strip()
+ return {
+ "source": "live",
+ "pos_id": str((row.get("raw") or {}).get("posId") or "").strip() or None,
+ "inst_id": inst_id,
+ "underlying": str(row.get("underlying") or inst_id.split("-")[0] or ""),
+ "opt_type": row.get("opt_type"),
+ "strike": row.get("strike"),
+ "sheets": int(abs(_safe_float(row.get("pos")) or 0)),
+ "eth_amount": row.get("eth_amount"),
+ "open_avg_px": row.get("avg_px"),
+ "open_avg_px_fmt": row.get("avg_px_fmt"),
+ "close_avg_px": None,
+ "close_avg_px_fmt": None,
+ "premium_paid": row.get("premium_paid"),
+ "premium_paid_fmt": row.get("premium_paid_fmt"),
+ "realized_pnl": row.get("upl"),
+ "pnl_ratio_pct": row.get("upl_ratio_pct"),
+ "status": "open",
+ "status_label": "持仓中",
+ "close_type": None,
+ "created_at": _ms_to_iso(open_ms),
+ "closed_at": None,
+ "close_ms": open_ms,
+ "tick_sz": row.get("tick_sz"),
+ "raw": row.get("raw"),
+ }
+
+
def resolve_option_close_from_history(
hist_rows: list[dict[str, Any]],
*,
@@ -947,7 +1132,12 @@ def transfer_main_sub_account(
return {"ok": False, "msg": str(e)}
-def format_position_row(pos: dict[str, Any], ct_mult: float = 0.01) -> dict[str, Any]:
+def format_position_row(
+ pos: dict[str, Any],
+ ct_mult: float = 0.01,
+ *,
+ tick_sz: Any = None,
+) -> dict[str, Any]:
from lib.options.options_pricing_lib import (
close_breakeven_idx,
expiry_breakeven_px,
@@ -971,7 +1161,7 @@ def format_position_row(pos: dict[str, Any], ct_mult: float = 0.01) -> dict[str,
strike = parsed_strike
eth_amount = round(abs(sheets) * ct_mult, 8)
premium_paid = (
- round(total_premium(avg, eth_amount), 4) if avg is not None and eth_amount > 0 else None
+ round(total_premium(avg, eth_amount), 8) if avg is not None and eth_amount > 0 else None
)
delta_pa = _safe_float(pos.get("deltaPA"))
expiry_be = expiry_breakeven_px(
@@ -996,6 +1186,11 @@ def format_position_row(pos: dict[str, Any], ct_mult: float = 0.01) -> dict[str,
"eth_amount": eth_amount,
"avg_px": avg,
"mark_px": mark,
+ "avg_px_fmt": format_option_px(avg, tick_sz) if avg is not None else None,
+ "mark_px_fmt": format_option_px(mark, tick_sz) if mark is not None else None,
+ "premium_paid_fmt": format_usdc_amount(premium_paid),
+ "tick_sz": tick_sz,
+ "ct_mult": ct_mult,
"idx_px": idx_px,
"premium_paid": premium_paid,
"upl": upl,
diff --git a/lib/options/options_register.py b/lib/options/options_register.py
index 8b19b29..b526fa2 100644
--- a/lib/options/options_register.py
+++ b/lib/options/options_register.py
@@ -189,6 +189,27 @@ def _refresh_position_avail(cfg: dict[str, Any], ex: Any, inst_id: str) -> int |
return _position_avail_sheets(pos)
+def _enrich_position_row_display(
+ cfg: dict[str, Any],
+ ex: Any,
+ raw_pos: dict[str, Any],
+ *,
+ meta_cache: dict[str, dict[str, Any] | None] | None = None,
+ premium_override: float | None = None,
+) -> dict[str, Any]:
+ from lib.exchange.okx_options_lib import format_position_row, tick_sz_and_ct_mult
+
+ inst_id = str(raw_pos.get("instId") or "").strip()
+ tick_sz, ct_mult = tick_sz_and_ct_mult(ex, inst_id, meta_cache)
+ row = format_position_row(raw_pos, ct_mult=ct_mult, tick_sz=tick_sz)
+ if premium_override is not None:
+ row["premium_paid"] = premium_override
+ from lib.exchange.okx_options_lib import format_usdc_amount
+
+ row["premium_paid_fmt"] = format_usdc_amount(premium_override)
+ return row
+
+
def _attach_close_preview(
cfg: dict[str, Any],
ex: Any,
@@ -469,24 +490,33 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
if raw is None:
return jsonify({"ok": False, "msg": "获取期权持仓失败"})
_sync_options_trades(cfg, raw_positions=raw)
- rows = [cfg["format_position_row"](p) for p in raw]
+ meta_cache: dict[str, dict[str, Any] | None] = {}
conn = cfg["get_db"]()
try:
- for row in rows:
- inst = row.get("inst_id")
- if not inst:
- continue
- rec = conn.execute(
- """
- SELECT premium_paid FROM options_trades
- WHERE inst_id = ? AND status = 'open'
- ORDER BY id DESC LIMIT 1
- """,
- (inst,),
- ).fetchone()
- if rec and rec["premium_paid"] is not None:
- row["premium_paid"] = round(float(rec["premium_paid"]), 4)
+ rows = []
+ for p in raw:
+ inst = str(p.get("instId") or "").strip()
+ premium_override = None
+ if inst:
+ rec = conn.execute(
+ """
+ SELECT premium_paid FROM options_trades
+ WHERE inst_id = ? AND status = 'open'
+ ORDER BY id DESC LIMIT 1
+ """,
+ (inst,),
+ ).fetchone()
+ if rec and rec["premium_paid"] is not None:
+ premium_override = float(rec["premium_paid"])
+ row = _enrich_position_row_display(
+ cfg,
+ ex,
+ p,
+ meta_cache=meta_cache,
+ premium_override=premium_override,
+ )
_attach_close_preview(cfg, ex, row, premium_paid=_safe_float(row.get("premium_paid")))
+ rows.append(row)
finally:
conn.close()
return jsonify({"ok": True, "positions": rows})
@@ -838,24 +868,66 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
ex, err = _require_options_ex(cfg)
if ex is None:
return jsonify({"ok": False, "msg": err})
- _sync_options_trades(cfg)
+ from lib.exchange.okx_options_lib import (
+ fetch_all_option_positions_history,
+ format_live_option_history_row,
+ format_option_history_row,
+ tick_sz_and_ct_mult,
+ )
+
+ meta_cache: dict[str, dict[str, Any] | None] = {}
+ items: list[dict[str, Any]] = []
+
+ raw_live = cfg["fetch_option_positions"](ex)
+ if raw_live is None:
+ return jsonify({"ok": False, "msg": "获取期权持仓失败"})
conn = cfg["get_db"]()
try:
- init_options_tables(conn)
- rows = conn.execute(
- """
- SELECT id, inst_id, underlying, opt_type, strike, sheets, eth_amount,
- open_quote, premium_paid, close_quote, premium_received,
- realized_pnl, status, signal_note, created_at, closed_at
- FROM options_trades
- ORDER BY id DESC
- LIMIT 200
- """
- ).fetchall()
- items = [dict(r) for r in rows]
+ for p in raw_live:
+ inst = str(p.get("instId") or "").strip()
+ premium_override = None
+ if inst:
+ rec = conn.execute(
+ """
+ SELECT premium_paid FROM options_trades
+ WHERE inst_id = ? AND status = 'open'
+ ORDER BY id DESC LIMIT 1
+ """,
+ (inst,),
+ ).fetchone()
+ if rec and rec["premium_paid"] is not None:
+ premium_override = float(rec["premium_paid"])
+ row = _enrich_position_row_display(
+ cfg,
+ ex,
+ p,
+ meta_cache=meta_cache,
+ premium_override=premium_override,
+ )
+ open_ms = None
+ ctime = p.get("cTime") or (row.get("raw") or {}).get("cTime")
+ try:
+ if ctime is not None and str(ctime).strip():
+ open_ms = int(float(ctime))
+ except (TypeError, ValueError):
+ open_ms = None
+ items.append(format_live_option_history_row(row, open_ms=open_ms))
finally:
conn.close()
- return jsonify({"ok": True, "history": items})
+
+ hist_raw = fetch_all_option_positions_history(ex, limit=200)
+ for raw in hist_raw:
+ inst_id = str(raw.get("instId") or "").strip()
+ tick_sz, ct_mult = tick_sz_and_ct_mult(ex, inst_id, meta_cache)
+ items.append(format_option_history_row(raw, tick_sz=tick_sz, ct_mult=ct_mult))
+
+ open_rows = [x for x in items if x.get("status") == "open"]
+ 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
+ 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)})
@app.route("/api/options/stats")
@lr
@@ -867,26 +939,13 @@ 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: int):
+ def api_options_history_delete(trade_id: str):
ex, err = _require_options_ex(cfg)
if ex is None:
return jsonify({"ok": False, "msg": err})
- conn = cfg["get_db"]()
- try:
- init_options_tables(conn)
- row = conn.execute(
- "SELECT id, status FROM options_trades WHERE id = ?",
- (trade_id,),
- ).fetchone()
- if not row:
- return jsonify({"ok": False, "msg": "记录不存在"})
- conn.execute("DELETE FROM options_trades WHERE id = ?", (trade_id,))
- conn.commit()
- finally:
- conn.close()
- return jsonify({"ok": True})
+ return jsonify({"ok": False, "msg": "历史仓位来自交易所,不支持本地删除"})
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 3cd32fc..cb898df 100644
--- a/lib/options/templates/options_panel.html
+++ b/lib/options/templates/options_panel.html
@@ -198,11 +198,10 @@
状态 |
盈亏 |
时间 |
- 操作 |
- | 加载中… |
+ | 加载中… |
@@ -212,4 +211,4 @@
-
+
diff --git a/tests/test_options_sync.py b/tests/test_options_sync.py
index 53c7bb7..a38b300 100644
--- a/tests/test_options_sync.py
+++ b/tests/test_options_sync.py
@@ -1,11 +1,44 @@
"""期权平仓/到期状态同步单测."""
import sqlite3
-from lib.exchange.okx_options_lib import resolve_option_close_from_history
+from lib.exchange.okx_options_lib import (
+ format_option_history_row,
+ format_usdc_amount,
+ resolve_option_close_from_history,
+)
from lib.options.options_db import init_options_tables
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"
+
+
+def test_format_option_history_row():
+ raw = {
+ "instId": "BTC-USD_UM-260710-62000-P",
+ "openAvgPx": "380",
+ "closeAvgPx": "0",
+ "closeTotalPos": "1",
+ "openMaxPos": "1",
+ "realizedPnl": "-3.99",
+ "pnlRatio": "-1.049",
+ "type": "2",
+ "cTime": "1784000000000",
+ "uTime": "1784088035000",
+ "posId": "pos-btc",
+ }
+ row = format_option_history_row(raw, tick_sz="0.1", ct_mult=0.01)
+ assert row["inst_id"] == "BTC-USD_UM-260710-62000-P"
+ assert row["sheets"] == 1
+ assert row["realized_pnl"] == -3.99
+ assert row["status_label"] == "已平"
+ assert row["open_avg_px_fmt"] == "380"
+ assert row["premium_paid_fmt"] == "3.8"
+
+
def test_resolve_option_close_from_history_picks_latest():
rows = [
{"instId": "ETH-USD_UM-260709-1700-P", "uTime": "1000", "realizedPnl": "-1.0", "closeAvgPx": "0"},