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:
@@ -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;
|
||||
|
||||
@@ -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" : "";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user