Fix false options close sync, restore bar layout, and show open premium in history.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-09 17:51:07 +08:00
parent 08a61fd4ae
commit 5a97e14f3e
6 changed files with 124 additions and 24 deletions
+12 -15
View File
@@ -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;
+6 -5
View File
@@ -485,12 +485,14 @@
'<span class="opt-pos-bar-id-group">' +
'<strong class="opt-pos-bar-title" title="' + inst + '">' + inst + "</strong>" +
'<span class="pos-side-badge ' + sideCls + '">' + optTypeLabel(p.opt_type) + "</span>" +
"</span></span>" +
"</span>" +
'<span class="opt-pos-bar-meta">行权 ' + fmt(p.strike, 0) + " · " + fmt(p.pos, 0) + "张</span>" +
"</span>" +
'<span class="opt-pos-bar-side">' +
(expAttr
? '<span class="opt-pos-bar-cd"><span class="opt-expiry-cd" data-opt-exp-ms="' + expAttr + '">—</span></span>'
? '<span class="opt-pos-bar-cd">到期 <span class="opt-expiry-cd" data-opt-exp-ms="' + expAttr + '">—</span></span>'
: "") +
'<span class="opt-pos-bar-pnl ' + uplCls + '">' + fmt(p.upl, 2) + "</span>" +
'<span class="opt-pos-bar-pnl ' + uplCls + '">' + fmt(p.upl, 2) + " USDC</span>" +
'<span class="opt-pos-bar-roi ' + uplCls + '">' +
(p.upl_ratio_pct != null ? fmt(p.upl_ratio_pct, 2) + "%" : "—") + "</span>" +
"</span>" +
@@ -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" : "";
+53
View File
@@ -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,
+4 -2
View File
@@ -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,
+1 -1
View File
@@ -137,4 +137,4 @@
</div>
</div>
<script src="/static/options_expiry_countdown.js?v=1"></script>
<script src="/static/options_panel.js?v=15"></script>
<script src="/static/options_panel.js?v=16"></script>
+48 -1
View File
@@ -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)