Persist options review deletes across local resync.

Hide deleted rows by history key and contract/close fingerprint so local options_trades imports no longer resurrect them.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-17 10:03:00 +08:00
parent 48afc8ebe3
commit d7272c9722
9 changed files with 339 additions and 14 deletions
+56
View File
@@ -1096,6 +1096,9 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
key = (history_key or "").strip()
if not key:
return jsonify({"ok": False, "msg": "缺少 history_key"})
data = request.get_json(silent=True) or {}
inst_id = str(data.get("inst_id") or request.args.get("inst_id") or "").strip() or None
closed_at = str(data.get("closed_at") or request.args.get("closed_at") or "").strip() or None
conn = cfg["get_db"]()
try:
init_options_tables(conn)
@@ -1103,6 +1106,59 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
"INSERT OR IGNORE INTO options_history_hidden (history_key) VALUES (?)",
(key,),
)
# 同步隐藏期权复盘,避免本地已平记录刷新后又出现
try:
from lib.options.options_review_lib import hide_review_keys
hide_review_keys(
conn,
history_key=key,
inst_id=inst_id,
closed_at=closed_at,
)
if inst_id:
# 去掉已导入的复盘快照(按合约+平仓时间)
if closed_at:
rows = conn.execute(
"""
SELECT id, history_key FROM options_review_trades
WHERE inst_id = ?
AND substr(COALESCE(closed_at,''),1,16) = substr(?,1,16)
""",
(inst_id, closed_at),
).fetchall()
else:
rows = conn.execute(
"""
SELECT id, history_key FROM options_review_trades
WHERE inst_id = ?
""",
(inst_id,),
).fetchall()
for r in rows:
conn.execute(
"DELETE FROM options_review_entries WHERE trade_id=?",
(int(r["id"]),),
)
conn.execute(
"DELETE FROM options_review_trades WHERE id=?",
(int(r["id"]),),
)
conn.execute(
"INSERT OR IGNORE INTO options_review_hidden(history_key, inst_id, closed_at) VALUES (?,?,?)",
(str(r["history_key"]), inst_id, (closed_at or "")[:19] or None),
)
fps = []
if closed_at:
fps.append(f"inst_close:{inst_id}:{closed_at[:16]}")
fps.append(f"inst:{inst_id}")
for fp in fps:
conn.execute(
"INSERT OR IGNORE INTO options_review_hidden(history_key, inst_id, closed_at) VALUES (?,?,?)",
(fp, inst_id, (closed_at or "")[:19] or None),
)
except Exception:
pass
conn.commit()
finally:
conn.close()
+16
View File
@@ -109,6 +109,22 @@ def init_options_review_tables(conn: sqlite3.Connection) -> None:
)
"""
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS options_review_hidden (
history_key TEXT PRIMARY KEY,
inst_id TEXT,
closed_at TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
"""
)
conn.execute(
"""
CREATE INDEX IF NOT EXISTS idx_options_review_hidden_inst
ON options_review_hidden(inst_id, closed_at)
"""
)
_ensure_column(conn, "options_review_trades", "linked_hedge_plan_id", "INTEGER")
_ensure_column(conn, "options_review_trades", "excluded_as_hedge_leg", "INTEGER DEFAULT 0")
_ensure_column(conn, "options_review_trades", "target_price_up", "REAL")
+146 -1
View File
@@ -83,11 +83,35 @@ def set_sync_state(conn: sqlite3.Connection, key: str, value: str) -> None:
)
def _purge_review_trade_by_key(conn: sqlite3.Connection, history_key: str) -> bool:
"""删除已导入的复盘快照(含复盘内容)."""
key = str(history_key or "").strip()
if not key:
return False
existing = conn.execute(
"SELECT id FROM options_review_trades WHERE history_key=?", (key,)
).fetchone()
if not existing:
return False
tid = int(existing["id"])
conn.execute("DELETE FROM options_review_entries WHERE trade_id=?", (tid,))
conn.execute("DELETE FROM options_review_trades WHERE id=?", (tid,))
return True
def upsert_option_history_row(conn: sqlite3.Connection, row: dict[str, Any]) -> str:
"""幂等写入纯期权快照;不触碰 options_review_entries."""
"""幂等写入纯期权快照;不触碰 options_review_entries;已隐藏的不再导入."""
history_key = str(row.get("history_key") or "").strip()
if not history_key:
return "skip"
if is_review_hidden(
conn,
history_key,
inst_id=str(row.get("inst_id") or "").strip() or None,
closed_at=row.get("closed_at") or row.get("created_at"),
):
# 若此前已导入,清掉,避免列表残留
return "purged" if _purge_review_trade_by_key(conn, history_key) else "hidden"
opened_at = row.get("created_at") or row.get("opened_at")
closed_at = row.get("closed_at")
pnl = _safe_float(row.get("realized_pnl"))
@@ -133,6 +157,120 @@ def upsert_option_history_row(conn: sqlite3.Connection, row: dict[str, Any]) ->
return "inserted"
def _close_fingerprint(inst_id: Any, closed_at: Any) -> str | None:
inst = str(inst_id or "").strip()
if not inst:
return None
closed = str(closed_at or "").strip()
if not closed:
return f"inst:{inst}"
# 精确到分钟,避免秒差导致漏匹配
return f"inst_close:{inst}:{closed[:16]}"
def is_review_hidden(
conn: sqlite3.Connection,
history_key: str,
*,
inst_id: str | None = None,
closed_at: Any = None,
) -> bool:
init_options_review_tables(conn)
key = str(history_key or "").strip()
if key and conn.execute(
"SELECT 1 FROM options_review_hidden WHERE history_key=? LIMIT 1", (key,)
).fetchone():
return True
fp = _close_fingerprint(inst_id, closed_at)
if fp and conn.execute(
"SELECT 1 FROM options_review_hidden WHERE history_key=? LIMIT 1", (fp,)
).fetchone():
return True
# 期权历史页删除:options_history_hidden,按合约指纹或原 key
try:
if key and conn.execute(
"SELECT 1 FROM options_history_hidden WHERE history_key=? LIMIT 1", (key,)
).fetchone():
return True
if fp and conn.execute(
"SELECT 1 FROM options_history_hidden WHERE history_key=? LIMIT 1", (fp,)
).fetchone():
return True
# 仅隐藏了 ex:posId 时,用合约+平仓时间在历史隐藏表无直接命中;
# 若指纹已写入 options_review_hidden(新删除路径)上面已覆盖.
# 兼容:inst 级隐藏
if inst_id:
inst_fp = f"inst:{str(inst_id).strip()}"
if conn.execute(
"SELECT 1 FROM options_review_hidden WHERE history_key=? LIMIT 1",
(inst_fp,),
).fetchone():
return True
if conn.execute(
"SELECT 1 FROM options_history_hidden WHERE history_key=? LIMIT 1",
(inst_fp,),
).fetchone():
return True
except Exception:
pass
return False
def hide_review_keys(
conn: sqlite3.Connection,
*,
history_key: str,
inst_id: str | None = None,
closed_at: Any = None,
) -> None:
init_options_review_tables(conn)
keys = [str(history_key or "").strip()]
fp = _close_fingerprint(inst_id, closed_at)
if fp:
keys.append(fp)
for k in keys:
if not k:
continue
conn.execute(
"""
INSERT OR IGNORE INTO options_review_hidden(history_key, inst_id, closed_at)
VALUES (?, ?, ?)
""",
(k, (inst_id or None), str(closed_at or "")[:19] or None),
)
try:
conn.execute(
"INSERT OR IGNORE INTO options_history_hidden(history_key) VALUES (?)",
(k,),
)
except Exception:
pass
def hide_review_trade(conn: sqlite3.Connection, trade_id: int) -> dict[str, Any]:
"""从复盘列表删除并持久隐藏,刷新本地源也不会再回来."""
init_options_review_tables(conn)
row = conn.execute(
"SELECT * FROM options_review_trades WHERE id=?", (int(trade_id),)
).fetchone()
if not row:
return {"ok": False, "msg": "记录不存在"}
d = _row_to_dict(row)
hide_review_keys(
conn,
history_key=str(d.get("history_key") or ""),
inst_id=str(d.get("inst_id") or "").strip() or None,
closed_at=d.get("closed_at") or d.get("opened_at"),
)
entry = conn.execute(
"SELECT * FROM options_review_entries WHERE trade_id=?", (int(trade_id),)
).fetchone()
conn.execute("DELETE FROM options_review_entries WHERE trade_id=?", (int(trade_id),))
conn.execute("DELETE FROM options_review_trades WHERE id=?", (int(trade_id),))
return {"ok": True, "entry": _row_to_dict(entry) if entry else None, "history_key": d.get("history_key")}
def sync_options_from_local_trades(conn: sqlite3.Connection) -> dict[str, Any]:
"""从本地 options_trades 已平仓记录导入复盘快照(不访问交易所)."""
init_options_review_tables(conn)
@@ -282,6 +420,13 @@ def upsert_hedge_plan_row(
return "skip"
opened_at = plan.get("opened_at") or plan.get("created_at")
closed_at = plan.get("closed_at")
if is_review_hidden(
conn,
history_key,
inst_id=None,
closed_at=closed_at,
):
return "purged" if _purge_review_trade_by_key(conn, history_key) else "hidden"
total = _safe_float(plan.get("realized_pnl_total"))
hold = _hold_seconds(opened_at, closed_at)
fields = {
+21
View File
@@ -22,6 +22,7 @@ from lib.options.options_review_lib import (
delete_review_entry,
ensure_local_review_synced,
get_review_trade,
hide_review_trade,
list_review_trades,
save_review_entry,
sync_all_review_sources,
@@ -168,6 +169,26 @@ def register_options_review_routes(app: Flask, cfg: dict[str, Any], repo_root: s
finally:
conn.close()
@app.route("/api/options/review/trades/<int:trade_id>", methods=["DELETE"])
@lr
def api_options_review_trade_hide(trade_id: int):
"""从复盘列表删除并持久隐藏(刷新本地源也不会再导入)."""
conn = cfg["get_db"]()
try:
out = hide_review_trade(conn, trade_id)
if out.get("ok"):
entry = out.get("entry") or {}
folder = options_review_upload_dir(cfg["upload_folder"])
for path in options_review_image_paths(entry, folder):
try:
os.remove(path)
except OSError:
pass
conn.commit()
return jsonify(out), (200 if out.get("ok") else 400)
finally:
conn.close()
@app.route("/api/options/review/entry/<int:trade_id>", methods=["DELETE"])
@lr
def api_options_review_entry_delete(trade_id: int):
+1 -1
View File
@@ -274,4 +274,4 @@
</div>
</div>
<script src="/static/options_expiry_countdown.js?v=1"></script>
<script src="/static/options_panel.js?v=38"></script>
<script src="/static/options_panel.js?v=39"></script>
@@ -157,14 +157,15 @@
<th>持有</th>
<th>策略</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody id="or-trades-tbody">
<tr><td colspan="7" class="muted">加载中…</td></tr>
<tr><td colspan="8" class="muted">加载中…</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<script src="/static/options_review.js?v=4"></script>
<script src="/static/options_review.js?v=6"></script>