From 5a97e14f3e4b107fdbeb9a72209907e5dd30de44 Mon Sep 17 00:00:00 2001 From: dekun Date: Thu, 9 Jul 2026 17:51:07 +0800 Subject: [PATCH] Fix false options close sync, restore bar layout, and show open premium in history. Co-authored-by: Cursor --- lib/common/static/instance_theme.css | 27 ++++++------ lib/common/static/options_panel.js | 11 ++--- lib/options/options_monitor_lib.py | 53 ++++++++++++++++++++++++ lib/options/options_register.py | 6 ++- lib/options/templates/options_panel.html | 2 +- tests/test_options_sync.py | 49 +++++++++++++++++++++- 6 files changed, 124 insertions(+), 24 deletions(-) diff --git a/lib/common/static/instance_theme.css b/lib/common/static/instance_theme.css index abe727b..24ce948 100644 --- a/lib/common/static/instance_theme.css +++ b/lib/common/static/instance_theme.css @@ -3303,9 +3303,9 @@ html[data-theme="light"] .options-estimate-row { .opt-pos-bar-main { display: flex; align-items: center; - gap: 6px; + gap: 8px; min-width: 0; - flex: 0 1 auto; + flex: 1 1 auto; overflow: hidden; } .opt-pos-bar-id-group { @@ -3313,7 +3313,7 @@ html[data-theme="light"] .options-estimate-row { align-items: center; gap: 6px; min-width: 0; - max-width: 100%; + flex-shrink: 1; overflow: hidden; } .opt-pos-bar .pos-side-badge { @@ -3325,17 +3325,19 @@ html[data-theme="light"] .options-estimate-row { font-size: 0.62rem; line-height: 1; } -.opt-pos-cards--accordion .opt-pos-bar-meta { - display: none; +.opt-pos-bar-meta { + font-size: 0.66rem; + color: #8b95b0; + white-space: nowrap; + flex-shrink: 0; } .opt-pos-bar-side { display: flex; - flex-direction: column; - align-items: flex-end; - justify-content: center; - gap: 1px; + flex-direction: row; + align-items: center; + gap: 10px; flex: 0 0 auto; - margin-left: auto; + margin-left: 8px; font-variant-numeric: tabular-nums; } .opt-pos-bar-title { @@ -3348,11 +3350,6 @@ html[data-theme="light"] .options-estimate-row { flex: 0 1 auto; min-width: 0; } -.opt-pos-bar-meta { - font-size: 0.66rem; - color: #8b95b0; - white-space: nowrap; -} .opt-pos-bar-cd { font-size: 0.66rem; color: #8b95b0; diff --git a/lib/common/static/options_panel.js b/lib/common/static/options_panel.js index f3b5822..1dbf6d3 100644 --- a/lib/common/static/options_panel.js +++ b/lib/common/static/options_panel.js @@ -485,12 +485,14 @@ '' + '' + inst + "" + '' + optTypeLabel(p.opt_type) + "" + - "" + + "" + + '行权 ' + fmt(p.strike, 0) + " · " + fmt(p.pos, 0) + "张" + + "" + '' + (expAttr - ? '' + ? '到期 ' : "") + - '' + fmt(p.upl, 2) + "" + + '' + fmt(p.upl, 2) + " USDC" + '' + (p.upl_ratio_pct != null ? fmt(p.upl_ratio_pct, 2) + "%" : "—") + "" + "" + @@ -679,8 +681,7 @@ } list.forEach(function (h) { const tr = document.createElement("tr"); - const prem = h.status === "closed" ? h.premium_received : h.premium_paid; - const premTxt = prem != null ? fmt(prem, 2) : "—"; + const premTxt = h.premium_paid != null ? fmt(h.premium_paid, 2) : "—"; const pnl = h.realized_pnl; const pnlTxt = pnl != null ? fmt(pnl, 2) : "—"; const pnlCls = pnl > 0 ? "pos-pnl-profit" : pnl < 0 ? "pos-pnl-loss" : ""; diff --git a/lib/options/options_monitor_lib.py b/lib/options/options_monitor_lib.py index e587863..c11f7f0 100644 --- a/lib/options/options_monitor_lib.py +++ b/lib/options/options_monitor_lib.py @@ -176,6 +176,8 @@ def sync_open_options_trades( closed_at = datetime.fromtimestamp(int(exp_ms) / 1000, tz=timezone.utc).strftime( "%Y-%m-%d %H:%M:%S" ) + else: + continue conn.execute( """ @@ -207,6 +209,57 @@ def sync_open_options_trades( return updated +def reconcile_live_open_trades( + conn: sqlite3.Connection, + *, + live_inst_ids: set[str], +) -> int: + """交易所有持仓但本地误标 closed 时恢复为 open.""" + fixed = 0 + for inst_id in live_inst_ids: + if not inst_id: + continue + open_row = conn.execute( + "SELECT id FROM options_trades WHERE inst_id = ? AND status = 'open' LIMIT 1", + (inst_id,), + ).fetchone() + if open_row: + continue + row = conn.execute( + """ + SELECT id, close_ord_id, realized_pnl + FROM options_trades + WHERE inst_id = ? AND status = 'closed' + ORDER BY id DESC LIMIT 1 + """, + (inst_id,), + ).fetchone() + if not row: + continue + if row["close_ord_id"]: + continue + if row["realized_pnl"] is not None: + continue + conn.execute( + """ + UPDATE options_trades + SET status = 'open', + close_quote = NULL, + premium_received = NULL, + realized_pnl = NULL, + closed_at = NULL, + signal_note = CASE + WHEN signal_note = '到期结算' THEN NULL + ELSE signal_note + END + WHERE id = ? + """, + (int(row["id"]),), + ) + fixed += 1 + return fixed + + def options_monitor_loop( *, enabled: bool, diff --git a/lib/options/options_register.py b/lib/options/options_register.py index 97045d1..162fb04 100644 --- a/lib/options/options_register.py +++ b/lib/options/options_register.py @@ -147,7 +147,7 @@ def _sync_options_trades(cfg: dict[str, Any]) -> None: if ex is None: return from lib.exchange.okx_options_lib import fetch_option_position_history - from lib.options.options_monitor_lib import sync_open_options_trades + from lib.options.options_monitor_lib import reconcile_live_open_trades, sync_open_options_trades raw = cfg["fetch_option_positions"](ex) live_ids = {str(p.get("instId") or "") for p in raw if str(p.get("instId") or "")} @@ -158,6 +158,7 @@ def _sync_options_trades(cfg: dict[str, Any]) -> None: conn = cfg["get_db"]() try: init_options_tables(conn) + reconcile_live_open_trades(conn, live_inst_ids=live_ids) sync_open_options_trades(conn, live_inst_ids=live_ids, fetch_history_fn=_hist) conn.commit() finally: @@ -715,13 +716,14 @@ def _start_monitor_thread(app: Flask, cfg: dict[str, Any]) -> None: def _sync(conn): from lib.exchange.okx_options_lib import fetch_option_position_history - from lib.options.options_monitor_lib import sync_open_options_trades + from lib.options.options_monitor_lib import reconcile_live_open_trades, sync_open_options_trades ex = cfg.get("exchange_options") if ex is None: return 0 raw = cfg["fetch_option_positions"](ex) live_ids = {str(p.get("instId") or "") for p in raw if str(p.get("instId") or "")} + reconcile_live_open_trades(conn, live_inst_ids=live_ids) return sync_open_options_trades( conn, live_inst_ids=live_ids, diff --git a/lib/options/templates/options_panel.html b/lib/options/templates/options_panel.html index 2d116e6..b7b488e 100644 --- a/lib/options/templates/options_panel.html +++ b/lib/options/templates/options_panel.html @@ -137,4 +137,4 @@ - + diff --git a/tests/test_options_sync.py b/tests/test_options_sync.py index 154902f..53c7bb7 100644 --- a/tests/test_options_sync.py +++ b/tests/test_options_sync.py @@ -45,7 +45,54 @@ def test_sync_open_options_trades_marks_expired_closed(): assert "到期结算" in (row["signal_note"] or "") -def test_sync_open_options_trades_uses_exchange_history(): +def test_sync_open_options_trades_skips_without_close_evidence(): + conn = sqlite3.connect(":memory:") + conn.row_factory = sqlite3.Row + init_options_tables(conn) + conn.execute( + """ + INSERT INTO options_trades + (inst_id, underlying, opt_type, strike, exp_time, sheets, eth_amount, + open_quote, premium_paid, status, created_at) + VALUES (?, 'BTC', 'P', 62000, '', 1, 0.01, 380.0, 3.8, 'open', '2026-07-09 08:00:00') + """, + ("BTC-USD_UM-260710-62000-P",), + ) + conn.commit() + + n = sync_open_options_trades( + conn, + live_inst_ids=set(), + fetch_history_fn=lambda _inst: [], + ) + assert n == 0 + row = conn.execute("SELECT status FROM options_trades").fetchone() + assert row["status"] == "open" + + +def test_reconcile_live_open_trades_reopens_sync_artifact(): + conn = sqlite3.connect(":memory:") + conn.row_factory = sqlite3.Row + init_options_tables(conn) + conn.execute( + """ + INSERT INTO options_trades + (inst_id, underlying, opt_type, strike, exp_time, sheets, eth_amount, + open_quote, premium_paid, status, closed_at) + VALUES (?, 'BTC', 'P', 62000, '', 1, 0.01, 380.0, 3.8, 'closed', '2026-07-09 09:10:34') + """, + ("BTC-USD_UM-260710-62000-P",), + ) + conn.commit() + + from lib.options.options_monitor_lib import reconcile_live_open_trades + + n = reconcile_live_open_trades(conn, live_inst_ids={"BTC-USD_UM-260710-62000-P"}) + assert n == 1 + row = conn.execute("SELECT status, closed_at FROM options_trades").fetchone() + assert row["status"] == "open" + assert row["closed_at"] is None + conn = sqlite3.connect(":memory:") conn.row_factory = sqlite3.Row init_options_tables(conn)