From 8842f0b869d871e1519b33fa3df1e551f0341fc2 Mon Sep 17 00:00:00 2001 From: dekun Date: Fri, 17 Jul 2026 10:50:12 +0800 Subject: [PATCH] Restructure options review into pending trades, form, and reviewed detail. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Show five pending trades per page, reveal the upload form only after clicking 复盘, and list saved reviews with image detail above stats. Co-authored-by: Cursor --- lib/common/static/options_review.js | 392 +++++++++++++++--- lib/options/options_review_lib.py | 2 + .../templates/options_review_panel.html | 102 +++-- 3 files changed, 406 insertions(+), 90 deletions(-) diff --git a/lib/common/static/options_review.js b/lib/common/static/options_review.js index a7c3b21..f67f1d4 100644 --- a/lib/common/static/options_review.js +++ b/lib/common/static/options_review.js @@ -1,10 +1,10 @@ /** - * OKX 期权复盘:三类 Tab + 列表点选 + 下方多周期(5m/15m/1h/4h)复盘表单. + * OKX 期权复盘:待复盘交易(5行) → 点复盘出表单 → 复盘记录详情 → 统计. */ (function (global) { "use strict"; - var SLOT_TFS = ["5m", "15m", "1h", "4h"]; + var PAGE_SIZE = 5; var TAB_LABELS = { option_spot: "期权交易记录", options_options: "期期对冲记录", @@ -27,11 +27,22 @@ var currentTradeId = null; var draftId = ""; var tradesCache = {}; + var reviewedCache = {}; + var tradesPage = 0; + var tradesHasMore = false; function $(id) { return document.getElementById(id); } + function escapeHtml(s) { + return String(s == null ? "" : s) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); + } + function fmtPnl(v) { if (v == null || v === "") return "—"; var n = Number(v); @@ -54,6 +65,23 @@ return s; } + function tradeTitle(t) { + if (!t) return "—"; + if (t.source_type === "option_spot") return t.inst_id || "—"; + return ( + (t.underlying || "") + + (t.direction ? " " + t.direction : "") + + (t.plan_close_reason ? " · " + t.plan_close_reason : "") + ); + } + + function pnlStyle(v) { + var n = Number(v); + if (n > 0) return "color:#3dd68c"; + if (n < 0) return "color:#f07178"; + return ""; + } + function newDraftId() { if (global.crypto && typeof global.crypto.randomUUID === "function") { return global.crypto.randomUUID().replace(/-/g, ""); @@ -63,23 +91,21 @@ return s; } - function qs() { + function baseQs() { var p = new URLSearchParams(); p.set("source_type", activeSource); var uly = ($("or-filter-uly") || {}).value || ""; var opt = ($("or-filter-opt") || {}).value || ""; - var reviewed = ($("or-filter-reviewed") || {}).value || ""; var strategy = (($("or-filter-strategy") || {}).value || "").trim(); var from = ($("or-filter-from") || {}).value || ""; var to = ($("or-filter-to") || {}).value || ""; if (uly) p.set("underlying", uly); if (opt) p.set("opt_type", opt); - if (reviewed) p.set("reviewed", reviewed); if (strategy) p.set("strategy_tag", strategy); if (from) p.set("closed_from", from.replace("T", " ") + ":00"); if (to) p.set("closed_to", to.replace("T", " ") + ":00"); if (($("or-include-hedge-legs") || {}).checked) p.set("include_hedge_legs", "1"); - return p.toString(); + return p; } function setSyncStatus(text) { @@ -90,6 +116,7 @@ function reloadAll() { setSyncStatus("读取本地记录…"); loadTrades(); + loadReviewed(); loadStats(); } @@ -170,6 +197,7 @@ function setActiveTab(source) { activeSource = source || "option_spot"; + tradesPage = 0; document.querySelectorAll(".or-tab").forEach(function (btn) { btn.classList.toggle("active", btn.getAttribute("data-source") === activeSource); }); @@ -177,45 +205,49 @@ if (title) title.textContent = TAB_LABELS[activeSource] || "记录"; applyFormPresets(activeSource); hideJournalForm(); + hideDetail(); reloadAll(); } + function updateTradesPager() { + var label = $("or-trades-page-label"); + var prev = $("or-trades-prev"); + var next = $("or-trades-next"); + if (label) label.textContent = "第 " + (tradesPage + 1) + " 页"; + if (prev) prev.disabled = tradesPage <= 0; + if (next) next.disabled = !tradesHasMore; + } + function loadTrades() { var tbody = $("or-trades-tbody"); if (!tbody) return; - tbody.innerHTML = '加载中…'; - fetch("/api/options/review/trades?" + qs(), { credentials: "same-origin" }) + tbody.innerHTML = '加载中…'; + var p = baseQs(); + p.set("reviewed", "0"); + p.set("limit", String(PAGE_SIZE)); + p.set("offset", String(tradesPage * PAGE_SIZE)); + fetch("/api/options/review/trades?" + p.toString(), { credentials: "same-origin" }) .then(function (r) { return r.json(); }) .then(function (data) { setSyncStatus("本地记录已加载"); if (!data.ok) { - tbody.innerHTML = '加载失败'; + tbody.innerHTML = '加载失败'; return; } var rows = data.trades || []; + tradesHasMore = rows.length >= PAGE_SIZE; tradesCache = {}; + updateTradesPager(); if (!rows.length) { tbody.innerHTML = - '暂无本地已平记录(期权页平仓后或对冲计划结束后会出现在此)'; + '暂无待复盘记录'; return; } tbody.innerHTML = rows .map(function (t) { tradesCache[t.id] = t; - var title = - t.source_type === "option_spot" - ? t.inst_id || "—" - : (t.underlying || "") + - (t.direction ? " " + t.direction : "") + - (t.plan_close_reason ? " · " + t.plan_close_reason : ""); - var pnlClass = - Number(t.realized_pnl_total) > 0 - ? "color:#3dd68c" - : Number(t.realized_pnl_total) < 0 - ? "color:#f07178" - : ""; var active = currentTradeId === t.id ? " or-row-active" : ""; return ( '' + "" + - (t.source_label || t.source_type) + + escapeHtml(t.source_label || t.source_type) + "" + "" + - title + + escapeHtml(tradeTitle(t)) + "" + '' + fmtPnl(t.realized_pnl_total) + "" + '' + - (t.opened_at || "—") + + escapeHtml(t.opened_at || "—") + "
" + - (t.closed_at || "—") + + escapeHtml(t.closed_at || "—") + "" + "" + fmtHold(t.hold_seconds) + "" + - "" + - (t.strategy_tag || "—") + - "" + - "" + - (t.reviewed ? "已复盘" : "待复盘") + - "" + - ' ' + + '' + "" ); }) .join(""); - tbody.querySelectorAll(".or-trade-row").forEach(function (tr) { - tr.addEventListener("click", function (ev) { - if (ev.target && ev.target.closest && ev.target.closest(".or-hide-btn")) return; - openJournalForm(Number(tr.getAttribute("data-id"))); + tbody.querySelectorAll(".or-review-btn").forEach(function (btn) { + btn.addEventListener("click", function (ev) { + ev.preventDefault(); + ev.stopPropagation(); + openJournalForm(Number(btn.getAttribute("data-id"))); }); }); tbody.querySelectorAll(".or-hide-btn").forEach(function (btn) { @@ -270,10 +300,217 @@ }); }) .catch(function () { - tbody.innerHTML = '加载失败'; + tbody.innerHTML = '加载失败'; }); } + function loadReviewed() { + var tbody = $("or-reviewed-tbody"); + if (!tbody) return; + tbody.innerHTML = '加载中…'; + var p = baseQs(); + p.set("reviewed", "1"); + p.set("limit", "100"); + p.set("offset", "0"); + fetch("/api/options/review/trades?" + p.toString(), { credentials: "same-origin" }) + .then(function (r) { + return r.json(); + }) + .then(function (data) { + if (!data.ok) { + tbody.innerHTML = '加载失败'; + return; + } + var rows = data.trades || []; + reviewedCache = {}; + if (!rows.length) { + tbody.innerHTML = '暂无复盘记录'; + return; + } + tbody.innerHTML = rows + .map(function (t) { + reviewedCache[t.id] = t; + return ( + '' + + "" + + escapeHtml(t.source_label || t.source_type) + + "" + + "" + + escapeHtml(tradeTitle(t)) + + "" + + '' + + fmtPnl(t.realized_pnl_total) + + "" + + "" + + escapeHtml(t.strategy_tag || "—") + + "" + + "" + + escapeHtml(t.result_tag || "—") + + "" + + '' + + escapeHtml(t.reviewed_at || "—") + + "" + + "" + ); + }) + .join(""); + tbody.querySelectorAll(".or-reviewed-row").forEach(function (tr) { + tr.addEventListener("click", function () { + openDetail(Number(tr.getAttribute("data-id"))); + }); + }); + }) + .catch(function () { + tbody.innerHTML = '加载失败'; + }); + } + + function hideDetail() { + var panel = $("or-detail-panel"); + if (panel) panel.classList.add("hidden"); + } + + function openDetail(tradeId) { + var panel = $("or-detail-panel"); + if (!panel) return; + panel.classList.remove("hidden"); + ($("or-detail-title") || {}).textContent = "加载中…"; + ($("or-detail-meta") || {}).innerHTML = ""; + ($("or-detail-text") || {}).innerHTML = ""; + ($("or-detail-images") || {}).innerHTML = ""; + panel.scrollIntoView({ behavior: "smooth", block: "nearest" }); + + fetch("/api/options/review/trades/" + tradeId, { credentials: "same-origin" }) + .then(function (r) { + return r.json(); + }) + .then(function (data) { + if (!data.ok || !data.trade) { + ($("or-detail-title") || {}).textContent = "加载失败"; + return; + } + renderDetail(data.trade); + }) + .catch(function () { + ($("or-detail-title") || {}).textContent = "加载失败"; + }); + } + + function renderDetail(t) { + var e = t.entry || {}; + reviewedCache[t.id] = t; + ($("or-detail-title") || {}).textContent = + "复盘详情 · " + (t.source_label || "") + " · " + tradeTitle(t); + var editBtn = $("or-detail-edit-btn"); + if (editBtn) editBtn.setAttribute("data-id", String(t.id)); + + var meta = $("or-detail-meta"); + if (meta) { + var cells = [ + ["标的", t.underlying || "—"], + ["合约/计划", tradeTitle(t)], + ["盈亏", fmtPnl(t.realized_pnl_total)], + ["持有", fmtHold(t.hold_seconds)], + ["开仓", t.opened_at || "—"], + ["平仓", t.closed_at || "—"], + ["策略", e.strategy_tag || "—"], + ["方向", e.direction_view || "—"], + ["结果", e.result_tag || "—"], + ["离场", e.exit_reason || "—"], + ["按计划", e.followed_plan || "—"], + ["入场逻辑", e.entry_logic || "—"], + ]; + if (t.is_hedge) { + cells.push(["永续盈亏", fmtPnl(t.realized_pnl_perp)]); + cells.push(["期权盈亏", fmtPnl(t.realized_pnl_options)]); + } + meta.innerHTML = cells + .map(function (pair) { + return ( + "
" + + escapeHtml(pair[0]) + + "
" + + escapeHtml(pair[1]) + + "
" + ); + }) + .join(""); + } + + var text = $("or-detail-text"); + if (text) { + var lines = []; + if (e.mistake_tags) lines.push("
心理标签:" + escapeHtml(e.mistake_tags) + "
"); + if (e.note) lines.push("
备注:" + escapeHtml(e.note).replace(/\n/g, "
") + "
"); + if (t.legs && t.legs.length) { + lines.push( + "
计划腿
" + + t.legs + .map(function (leg) { + return ( + "" + ); + }) + .join("") + + "
合约盈亏原因
" + + escapeHtml(leg.leg_role || "") + + "" + + escapeHtml(leg.inst_id || leg.symbol || "") + + "" + + fmtPnl(leg.realized_pnl) + + "" + + escapeHtml(leg.close_reason || "") + + "
" + ); + } + text.innerHTML = lines.join("") || '
无额外备注
'; + } + + var imagesHost = $("or-detail-images"); + if (imagesHost) { + var images = e.images || []; + if (!images.length) { + imagesHost.innerHTML = '
无截图
'; + } else { + imagesHost.innerHTML = images + .map(function (img) { + var file = String(img.file || "").trim(); + if (!file) return ""; + var src = "/static/images/options_journal/" + encodeURIComponent(file).replace(/%2F/g, "/"); + var label = escapeHtml(img.tf || "截图"); + return ( + '
' + + '' + + label + + "" + + '' +
+              label +
+              '' + + "
" + ); + }) + .join(""); + imagesHost.querySelectorAll("img").forEach(function (img) { + img.addEventListener("click", function () { + if (typeof global.showImage === "function") { + global.showImage(img.getAttribute("data-src")); + } else { + global.open(img.getAttribute("data-src"), "_blank"); + } + }); + }); + } + } + } + function renderGroup(title, items) { if (!items || !items.length) { return ( @@ -288,7 +525,7 @@ return ( '
' + "" + - g.key + + escapeHtml(g.key) + " · " + g.count + "笔" + @@ -314,7 +551,7 @@ var kpi = $("or-kpi"); var groups = $("or-stats-groups"); if (!kpi || !groups) return; - fetch("/api/options/review/stats?" + qs(), { credentials: "same-origin" }) + fetch("/api/options/review/stats?" + baseQs().toString(), { credentials: "same-origin" }) .then(function (r) { return r.json(); }) @@ -372,7 +609,7 @@ input.dataset.orBound = "1"; input.addEventListener("change", function () { var file = input.files && input.files[0]; - var row = input.closest(".or-upload-row"); + var row = input.closest(".journal-upload-row"); var status = row && row.querySelector(".or-upload-status"); var hidden = row && row.querySelector(".or-upload-hidden"); if (!file) { @@ -456,7 +693,7 @@ function hideJournalForm() { currentTradeId = null; var card = $("or-journal-card"); - if (card) card.classList.add("or-journal-idle"); + if (card) card.classList.add("hidden"); document.querySelectorAll(".or-trade-row").forEach(function (tr) { tr.classList.remove("or-row-active"); }); @@ -478,8 +715,7 @@ resetUploadSlots(); var summary = $("or-journal-summary"); if (summary) { - summary.textContent = - "点下方列表一笔记录自动填入;截图槽位与合约复盘相同(5m / 15m / 1h / 4h)."; + summary.textContent = "截图槽位与合约复盘相同(5m / 15m / 1h / 4h)."; } var legsHost = $("or-legs-host"); if (legsHost) legsHost.innerHTML = ""; @@ -490,7 +726,7 @@ currentTradeId = tradeId; var card = $("or-journal-card"); if (!card) return; - card.classList.remove("or-journal-idle"); + card.classList.remove("hidden"); card.scrollIntoView({ behavior: "smooth", block: "start" }); document.querySelectorAll(".or-trade-row").forEach(function (tr) { tr.classList.toggle("or-row-active", Number(tr.getAttribute("data-id")) === tradeId); @@ -551,7 +787,6 @@ : ""); } - // 已有截图回填状态(文件名保留,新上传会换 draft) (e.images || []).forEach(function (img) { var hidden = document.querySelector( '#or-upload-slots .or-upload-hidden[data-tf="' + img.tf + '"]' @@ -574,13 +809,13 @@ .map(function (leg) { return ( "" + - (leg.leg_role || "") + + escapeHtml(leg.leg_role || "") + "" + - (leg.inst_id || leg.symbol || "") + + escapeHtml(leg.inst_id || leg.symbol || "") + "" + fmtPnl(leg.realized_pnl) + "" + - (leg.close_reason || "") + + escapeHtml(leg.close_reason || "") + "" ); }) @@ -594,7 +829,7 @@ function hideTrade(tradeId) { if (!tradeId) return; - if (!confirm("从复盘列表删除并隐藏?刷新后也不会再出现.")) return; + if (!confirm("从待复盘列表删除并隐藏?刷新后也不会再出现.")) return; fetch("/api/options/review/trades/" + tradeId, { method: "DELETE", credentials: "same-origin", @@ -618,7 +853,7 @@ function saveEntry() { var tradeId = Number(($("or-trade-id") || {}).value || 0); if (!tradeId) { - alert("请先在下方列表点选一笔本地记录"); + alert("请先点击交易记录中的「复盘」"); return; } var strategy = (($("or-f-strategy") || {}).value || "").trim(); @@ -654,7 +889,9 @@ return; } ($("or-save-status") || {}).textContent = "已保存"; + hideJournalForm(); reloadAll(); + openDetail(tradeId); }) .catch(function () { ($("or-save-status") || {}).textContent = "保存失败"; @@ -664,7 +901,7 @@ function deleteEntry() { var tradeId = Number(($("or-trade-id") || {}).value || 0); if (!tradeId) return; - if (!confirm("删除该条复盘内容与图片?")) return; + if (!confirm("删除该条复盘内容与图片?交易记录会回到待复盘列表.")) return; fetch("/api/options/review/entry/" + tradeId, { method: "DELETE", credentials: "same-origin", @@ -674,6 +911,7 @@ }) .then(function () { hideJournalForm(); + hideDetail(); reloadAll(); }); } @@ -689,18 +927,56 @@ var saveBtn = $("or-save-btn"); var clearBtn = $("or-clear-btn"); var delBtn = $("or-del-btn"); + var prevBtn = $("or-trades-prev"); + var nextBtn = $("or-trades-next"); + var detailClose = $("or-detail-close-btn"); + var detailEdit = $("or-detail-edit-btn"); if (reloadBtn) reloadBtn.addEventListener("click", reloadAll); if (saveBtn) saveBtn.addEventListener("click", saveEntry); if (clearBtn) clearBtn.addEventListener("click", hideJournalForm); if (delBtn) delBtn.addEventListener("click", deleteEntry); - ["or-filter-uly", "or-filter-opt", "or-filter-reviewed", "or-include-hedge-legs"].forEach( - function (id) { - var el = $(id); - if (el) el.addEventListener("change", reloadAll); + if (prevBtn) { + prevBtn.addEventListener("click", function () { + if (tradesPage <= 0) return; + tradesPage -= 1; + loadTrades(); + }); + } + if (nextBtn) { + nextBtn.addEventListener("click", function () { + if (!tradesHasMore) return; + tradesPage += 1; + loadTrades(); + }); + } + if (detailClose) detailClose.addEventListener("click", hideDetail); + if (detailEdit) { + detailEdit.addEventListener("click", function () { + var id = Number(detailEdit.getAttribute("data-id") || 0); + if (id) openJournalForm(id); + }); + } + ["or-filter-uly", "or-filter-opt", "or-include-hedge-legs"].forEach(function (id) { + var el = $(id); + if (el) { + el.addEventListener("change", function () { + tradesPage = 0; + reloadAll(); + }); } - ); + }); + ["or-filter-strategy", "or-filter-from", "or-filter-to"].forEach(function (id) { + var el = $(id); + if (el) { + el.addEventListener("change", function () { + tradesPage = 0; + reloadAll(); + }); + } + }); bindUploadSlots(); hideJournalForm(); + hideDetail(); setActiveTab("option_spot"); } diff --git a/lib/options/options_review_lib.py b/lib/options/options_review_lib.py index 6208e55..e699685 100644 --- a/lib/options/options_review_lib.py +++ b/lib/options/options_review_lib.py @@ -582,10 +582,12 @@ def enrich_trade_row(row: dict[str, Any], entry: dict[str, Any] | None = None) - out["entry"]["images"] = parse_options_review_images_json(entry.get("images_json")) out["strategy_tag"] = entry.get("strategy_tag") out["result_tag"] = entry.get("result_tag") + out["reviewed_at"] = entry.get("reviewed_at") or entry.get("updated_at") else: out["entry"] = None out["strategy_tag"] = None out["result_tag"] = None + out["reviewed_at"] = None return out diff --git a/lib/options/templates/options_review_panel.html b/lib/options/templates/options_review_panel.html index 2233909..8620d5b 100644 --- a/lib/options/templates/options_review_panel.html +++ b/lib/options/templates/options_review_panel.html @@ -1,4 +1,4 @@ -{# OKX 期权复盘:读本地记录;复盘上传区字体缩小,截图槽位对齐合约 #} +{# OKX 期权复盘:交易记录(5行) → 点复盘出表单 → 复盘记录 → 统计 #}
{% if not options_enabled %}
期权未启用:请设置 OKX_OPTIONS_ENABLED=true 后重启.
@@ -7,7 +7,7 @@ + {# 1. 交易记录(含 Tab/筛选,固定约5行) #}

期权复盘

@@ -41,8 +50,8 @@
-

直接读取本地已平期权与已结束对冲计划,无需同步交易所.点下方记录可填入复盘表单.

-
+

待复盘交易(每页5条).点「复盘」填写表单;保存后进入下方复盘记录.

+
- @@ -65,11 +69,35 @@ 含已归属对冲的期权腿
+

期权交易记录

+
+ + + + + + + + + + + + + + +
类型标的/合约盈亏开/平持有操作
加载中…
+
+
+ + 1 + +
-
+ {# 2. 复盘上传(默认隐藏,点交易「复盘」后显示) #} + -

可只传部分周期;选文件后即时上传,保存后详情页四宫格查看

+

可只传部分周期;选文件后即时上传

@@ -139,8 +167,8 @@
- - + +
@@ -148,35 +176,45 @@
+ {# 3. 已复盘记录 + 详情 #}
-

统计

-
-
-
- -
-

期权交易记录

-

点选一行,上方复盘表单自动填入.

+

复盘记录

+

保存后出现在此.点一行查看详情(含截图与完整复盘内容).

- +
- - - - + + - - + +
类型 标的/合约 盈亏开/平持有 策略状态操作结果复盘时间
加载中…
加载中…
+ +
+ + {# 4. 统计 #} +
+

统计

+
+
- +