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 (
+ "| " +
+ escapeHtml(leg.leg_role || "") +
+ " | " +
+ escapeHtml(leg.inst_id || leg.symbol || "") +
+ " | " +
+ fmtPnl(leg.realized_pnl) +
+ " | " +
+ escapeHtml(leg.close_reason || "") +
+ " |
"
+ );
+ })
+ .join("") +
+ "
"
+ );
+ }
+ 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 +
+ "" +
+ '

' +
+ "
"
+ );
+ })
+ .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 @@
-
直接读取本地已平期权与已结束对冲计划,无需同步交易所.点下方记录可填入复盘表单.
-
-
+ {# 2. 复盘上传(默认隐藏,点交易「复盘」后显示) #}
+
复盘记录上传(含截图)
-
点下方列表一笔记录自动填入;截图槽位与合约复盘相同(5m / 15m / 1h / 4h).
+
截图槽位与合约复盘相同(5m / 15m / 1h / 4h).
{% endfor %}
-
可只传部分周期;选文件后即时上传,保存后详情页四宫格查看
+
可只传部分周期;选文件后即时上传
@@ -139,8 +167,8 @@
-
-
+
+
@@ -148,35 +176,45 @@
+ {# 3. 已复盘记录 + 详情 #}
-
-
-
期权交易记录
-
点选一行,上方复盘表单自动填入.
+
复盘记录
+
保存后出现在此.点一行查看详情(含截图与完整复盘内容).
-
+
| 类型 |
标的/合约 |
盈亏 |
- 开/平 |
- 持有 |
策略 |
- 状态 |
- 操作 |
+ 结果 |
+ 复盘时间 |
-
- | 加载中… |
+
+ | 加载中… |
+
+
+
复盘详情
+
+
+
+
+
+
+
+
+
+ {# 4. 统计 #}
+
-
+