Restructure options review into pending trades, form, and reviewed detail.
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 <cursoragent@cursor.com>
This commit is contained in:
@@ -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, ">")
|
||||
.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 = '<tr><td colspan="8" class="muted">加载中…</td></tr>';
|
||||
fetch("/api/options/review/trades?" + qs(), { credentials: "same-origin" })
|
||||
tbody.innerHTML = '<tr><td colspan="6" class="muted">加载中…</td></tr>';
|
||||
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 = '<tr><td colspan="8" class="muted">加载失败</td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan="6" class="muted">加载失败</td></tr>';
|
||||
return;
|
||||
}
|
||||
var rows = data.trades || [];
|
||||
tradesHasMore = rows.length >= PAGE_SIZE;
|
||||
tradesCache = {};
|
||||
updateTradesPager();
|
||||
if (!rows.length) {
|
||||
tbody.innerHTML =
|
||||
'<tr><td colspan="8" class="muted">暂无本地已平记录(期权页平仓后或对冲计划结束后会出现在此)</td></tr>';
|
||||
'<tr><td colspan="6" class="muted">暂无待复盘记录</td></tr>';
|
||||
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 (
|
||||
'<tr class="or-trade-row' +
|
||||
@@ -224,41 +256,39 @@
|
||||
t.id +
|
||||
'">' +
|
||||
"<td><span class=\"or-badge\">" +
|
||||
(t.source_label || t.source_type) +
|
||||
escapeHtml(t.source_label || t.source_type) +
|
||||
"</span></td>" +
|
||||
"<td>" +
|
||||
title +
|
||||
escapeHtml(tradeTitle(t)) +
|
||||
"</td>" +
|
||||
'<td style="' +
|
||||
pnlClass +
|
||||
pnlStyle(t.realized_pnl_total) +
|
||||
'">' +
|
||||
fmtPnl(t.realized_pnl_total) +
|
||||
"</td>" +
|
||||
'<td class="muted" style="font-size:12px">' +
|
||||
(t.opened_at || "—") +
|
||||
escapeHtml(t.opened_at || "—") +
|
||||
"<br>" +
|
||||
(t.closed_at || "—") +
|
||||
escapeHtml(t.closed_at || "—") +
|
||||
"</td>" +
|
||||
"<td>" +
|
||||
fmtHold(t.hold_seconds) +
|
||||
"</td>" +
|
||||
"<td>" +
|
||||
(t.strategy_tag || "—") +
|
||||
"</td>" +
|
||||
"<td>" +
|
||||
(t.reviewed ? "已复盘" : "待复盘") +
|
||||
"</td>" +
|
||||
'<td><button type="button" class="btn-secondary or-hide-btn" data-id="' +
|
||||
'<td><button type="button" class="btn or-review-btn" data-id="' +
|
||||
t.id +
|
||||
'" style="font-size:.72rem;padding:2px 8px">复盘</button> ' +
|
||||
'<button type="button" class="btn-secondary or-hide-btn" data-id="' +
|
||||
t.id +
|
||||
'" style="font-size:.72rem;padding:2px 8px">删除</button></td>' +
|
||||
"</tr>"
|
||||
);
|
||||
})
|
||||
.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 = '<tr><td colspan="8" class="muted">加载失败</td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan="6" class="muted">加载失败</td></tr>';
|
||||
});
|
||||
}
|
||||
|
||||
function loadReviewed() {
|
||||
var tbody = $("or-reviewed-tbody");
|
||||
if (!tbody) return;
|
||||
tbody.innerHTML = '<tr><td colspan="6" class="muted">加载中…</td></tr>';
|
||||
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 = '<tr><td colspan="6" class="muted">加载失败</td></tr>';
|
||||
return;
|
||||
}
|
||||
var rows = data.trades || [];
|
||||
reviewedCache = {};
|
||||
if (!rows.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="6" class="muted">暂无复盘记录</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = rows
|
||||
.map(function (t) {
|
||||
reviewedCache[t.id] = t;
|
||||
return (
|
||||
'<tr class="or-reviewed-row" data-id="' +
|
||||
t.id +
|
||||
'">' +
|
||||
"<td><span class=\"or-badge\">" +
|
||||
escapeHtml(t.source_label || t.source_type) +
|
||||
"</span></td>" +
|
||||
"<td>" +
|
||||
escapeHtml(tradeTitle(t)) +
|
||||
"</td>" +
|
||||
'<td style="' +
|
||||
pnlStyle(t.realized_pnl_total) +
|
||||
'">' +
|
||||
fmtPnl(t.realized_pnl_total) +
|
||||
"</td>" +
|
||||
"<td>" +
|
||||
escapeHtml(t.strategy_tag || "—") +
|
||||
"</td>" +
|
||||
"<td>" +
|
||||
escapeHtml(t.result_tag || "—") +
|
||||
"</td>" +
|
||||
'<td class="muted" style="font-size:12px">' +
|
||||
escapeHtml(t.reviewed_at || "—") +
|
||||
"</td>" +
|
||||
"</tr>"
|
||||
);
|
||||
})
|
||||
.join("");
|
||||
tbody.querySelectorAll(".or-reviewed-row").forEach(function (tr) {
|
||||
tr.addEventListener("click", function () {
|
||||
openDetail(Number(tr.getAttribute("data-id")));
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(function () {
|
||||
tbody.innerHTML = '<tr><td colspan="6" class="muted">加载失败</td></tr>';
|
||||
});
|
||||
}
|
||||
|
||||
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 (
|
||||
"<div><div class=\"muted\" style=\"font-size:11px\">" +
|
||||
escapeHtml(pair[0]) +
|
||||
"</div><div>" +
|
||||
escapeHtml(pair[1]) +
|
||||
"</div></div>"
|
||||
);
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
var text = $("or-detail-text");
|
||||
if (text) {
|
||||
var lines = [];
|
||||
if (e.mistake_tags) lines.push("<div><b>心理标签</b>:" + escapeHtml(e.mistake_tags) + "</div>");
|
||||
if (e.note) lines.push("<div><b>备注</b>:" + escapeHtml(e.note).replace(/\n/g, "<br>") + "</div>");
|
||||
if (t.legs && t.legs.length) {
|
||||
lines.push(
|
||||
"<div style=\"margin-top:6px\"><b>计划腿</b></div><table class=\"options-strike-table\"><thead><tr><th>腿</th><th>合约</th><th>盈亏</th><th>原因</th></tr></thead><tbody>" +
|
||||
t.legs
|
||||
.map(function (leg) {
|
||||
return (
|
||||
"<tr><td>" +
|
||||
escapeHtml(leg.leg_role || "") +
|
||||
"</td><td>" +
|
||||
escapeHtml(leg.inst_id || leg.symbol || "") +
|
||||
"</td><td>" +
|
||||
fmtPnl(leg.realized_pnl) +
|
||||
"</td><td>" +
|
||||
escapeHtml(leg.close_reason || "") +
|
||||
"</td></tr>"
|
||||
);
|
||||
})
|
||||
.join("") +
|
||||
"</tbody></table>"
|
||||
);
|
||||
}
|
||||
text.innerHTML = lines.join("") || '<div class="muted">无额外备注</div>';
|
||||
}
|
||||
|
||||
var imagesHost = $("or-detail-images");
|
||||
if (imagesHost) {
|
||||
var images = e.images || [];
|
||||
if (!images.length) {
|
||||
imagesHost.innerHTML = '<div class="muted">无截图</div>';
|
||||
} 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 (
|
||||
'<div class="or-detail-img-cell">' +
|
||||
'<span class="or-detail-img-label">' +
|
||||
label +
|
||||
"</span>" +
|
||||
'<img class="or-detail-img-thumb" src="' +
|
||||
src +
|
||||
'" alt="' +
|
||||
label +
|
||||
'" data-src="' +
|
||||
src +
|
||||
'">' +
|
||||
"</div>"
|
||||
);
|
||||
})
|
||||
.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 (
|
||||
'<div style="display:flex;justify-content:space-between;gap:8px;font-size:13px">' +
|
||||
"<span>" +
|
||||
g.key +
|
||||
escapeHtml(g.key) +
|
||||
" · " +
|
||||
g.count +
|
||||
"笔</span>" +
|
||||
@@ -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 (
|
||||
"<tr><td>" +
|
||||
(leg.leg_role || "") +
|
||||
escapeHtml(leg.leg_role || "") +
|
||||
"</td><td>" +
|
||||
(leg.inst_id || leg.symbol || "") +
|
||||
escapeHtml(leg.inst_id || leg.symbol || "") +
|
||||
"</td><td>" +
|
||||
fmtPnl(leg.realized_pnl) +
|
||||
"</td><td>" +
|
||||
(leg.close_reason || "") +
|
||||
escapeHtml(leg.close_reason || "") +
|
||||
"</td></tr>"
|
||||
);
|
||||
})
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{# OKX 期权复盘:读本地记录;复盘上传区字体缩小,截图槽位对齐合约 #}
|
||||
{# OKX 期权复盘:交易记录(5行) → 点复盘出表单 → 复盘记录 → 统计 #}
|
||||
<div class="options-review-wrap" id="options-review-root" style="grid-column:1/-1">
|
||||
{% if not options_enabled %}
|
||||
<div class="flash" style="margin-bottom:12px;font-size:.82rem">期权未启用:请设置 <code>OKX_OPTIONS_ENABLED=true</code> 后重启.</div>
|
||||
@@ -7,7 +7,7 @@
|
||||
<style>
|
||||
.options-review-wrap{font-size:.82rem}
|
||||
.options-review-wrap h2{font-size:1rem;margin:0 0 8px}
|
||||
.options-review-wrap h3{font-size:.9rem}
|
||||
.options-review-wrap h3{font-size:.9rem;margin:0 0 8px}
|
||||
.or-tabs{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px}
|
||||
.or-tab{border:1px solid rgba(127,127,127,.35);background:transparent;color:inherit;padding:5px 10px;border-radius:6px;cursor:pointer;font-size:.78rem}
|
||||
.or-tab.active{background:rgba(59,130,246,.25);border-color:rgba(59,130,246,.55)}
|
||||
@@ -15,7 +15,6 @@
|
||||
.or-stat-card{border:1px solid rgba(127,127,127,.25);border-radius:8px;padding:8px;font-size:.78rem}
|
||||
.or-trades-table{font-size:.78rem}
|
||||
.or-trades-table tr.or-row-active{outline:1px solid rgba(59,130,246,.55);background:rgba(59,130,246,.08)}
|
||||
.or-trades-table tbody tr{cursor:pointer}
|
||||
.or-journal-card{font-size:.78rem}
|
||||
.or-journal-card h2{font-size:.92rem}
|
||||
.or-journal-card input,
|
||||
@@ -27,9 +26,19 @@
|
||||
.or-journal-card .or-mood-grid{display:flex;flex-wrap:wrap;gap:6px 12px;margin:8px 0;font-size:.74rem}
|
||||
.or-journal-card .muted,
|
||||
.or-journal-card .sub{font-size:.7rem}
|
||||
.or-journal-idle .or-journal-body{opacity:.78}
|
||||
.or-journal-card.hidden{display:none!important}
|
||||
.or-reviewed-table tbody tr{cursor:pointer}
|
||||
.or-detail-panel{margin-top:10px;padding-top:10px;border-top:1px solid rgba(127,127,127,.25)}
|
||||
.or-detail-panel.hidden{display:none!important}
|
||||
.or-detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:6px 12px;font-size:.76rem;margin-bottom:8px}
|
||||
.or-detail-images{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:8px;margin:8px 0}
|
||||
.or-detail-img-cell{border:1px solid rgba(127,127,127,.25);border-radius:6px;padding:6px;text-align:center}
|
||||
.or-detail-img-label{display:block;font-size:.7rem;margin-bottom:4px;opacity:.8}
|
||||
.or-detail-img-thumb{max-width:100%;max-height:160px;border-radius:4px;cursor:pointer}
|
||||
.or-pager{display:flex;align-items:center;gap:8px;margin-top:8px;font-size:.74rem}
|
||||
</style>
|
||||
|
||||
{# 1. 交易记录(含 Tab/筛选,固定约5行) #}
|
||||
<div class="card" style="margin-bottom:10px">
|
||||
<div class="form-row" style="flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:6px">
|
||||
<h2 style="margin:0;margin-right:auto">期权复盘</h2>
|
||||
@@ -41,8 +50,8 @@
|
||||
<button type="button" class="or-tab" data-source="options_options" role="tab">期期对冲记录</button>
|
||||
<button type="button" class="or-tab" data-source="perp_options" role="tab">永期对冲记录</button>
|
||||
</div>
|
||||
<p class="muted" style="margin:0 0 8px;font-size:.72rem">直接读取本地已平期权与已结束对冲计划,无需同步交易所.点下方记录可填入复盘表单.</p>
|
||||
<div class="form-row" style="flex-wrap:wrap;gap:6px">
|
||||
<p class="muted" style="margin:0 0 8px;font-size:.72rem">待复盘交易(每页5条).点「复盘」填写表单;保存后进入下方复盘记录.</p>
|
||||
<div class="form-row" style="flex-wrap:wrap;gap:6px;margin-bottom:8px">
|
||||
<select id="or-filter-uly" style="font-size:.76rem">
|
||||
<option value="">标的:全部</option>
|
||||
<option value="ETH">ETH</option>
|
||||
@@ -53,11 +62,6 @@
|
||||
<option value="C">Call</option>
|
||||
<option value="P">Put</option>
|
||||
</select>
|
||||
<select id="or-filter-reviewed" style="font-size:.76rem">
|
||||
<option value="">复盘:全部</option>
|
||||
<option value="0">待复盘</option>
|
||||
<option value="1">已复盘</option>
|
||||
</select>
|
||||
<input type="text" id="or-filter-strategy" placeholder="策略标签" style="max-width:110px;font-size:.76rem">
|
||||
<input type="datetime-local" id="or-filter-from" title="平仓起" style="font-size:.76rem">
|
||||
<input type="datetime-local" id="or-filter-to" title="平仓止" style="font-size:.76rem">
|
||||
@@ -65,11 +69,35 @@
|
||||
<input type="checkbox" id="or-include-hedge-legs"> 含已归属对冲的期权腿
|
||||
</label>
|
||||
</div>
|
||||
<h3 id="or-list-title" style="margin-top:0">期权交易记录</h3>
|
||||
<div class="options-strike-table-wrap">
|
||||
<table class="options-strike-table or-trades-table" id="or-trades-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>类型</th>
|
||||
<th>标的/合约</th>
|
||||
<th>盈亏</th>
|
||||
<th>开/平</th>
|
||||
<th>持有</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="or-trades-tbody">
|
||||
<tr><td colspan="6" class="muted">加载中…</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="or-pager" id="or-trades-pager">
|
||||
<button type="button" class="btn-secondary" id="or-trades-prev" style="font-size:.72rem;padding:2px 8px">上一页</button>
|
||||
<span class="muted" id="or-trades-page-label">1</span>
|
||||
<button type="button" class="btn-secondary" id="or-trades-next" style="font-size:.72rem;padding:2px 8px">下一页</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card journal-card or-journal-card or-journal-idle" id="or-journal-card" style="margin-bottom:10px">
|
||||
{# 2. 复盘上传(默认隐藏,点交易「复盘」后显示) #}
|
||||
<div class="card journal-card or-journal-card hidden" id="or-journal-card" style="margin-bottom:10px">
|
||||
<h2>复盘记录上传(含截图)</h2>
|
||||
<p class="muted" id="or-journal-summary" style="margin-top:0">点下方列表一笔记录自动填入;截图槽位与合约复盘相同(5m / 15m / 1h / 4h).</p>
|
||||
<p class="muted" id="or-journal-summary" style="margin-top:0">截图槽位与合约复盘相同(5m / 15m / 1h / 4h).</p>
|
||||
<div class="or-journal-body">
|
||||
<form id="or-journal-form" onsubmit="return false;">
|
||||
<input type="hidden" id="or-trade-id" value="">
|
||||
@@ -126,7 +154,7 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<p class="sub journal-upload-hint">可只传部分周期;选文件后即时上传,保存后详情页四宫格查看</p>
|
||||
<p class="sub journal-upload-hint">可只传部分周期;选文件后即时上传</p>
|
||||
|
||||
<div class="or-mood-grid mood-grid">
|
||||
<label><input type="checkbox" class="or-mood" value="怕踏空">怕踏空</label>
|
||||
@@ -139,8 +167,8 @@
|
||||
<textarea id="or-f-note" rows="2" placeholder="备注" style="width:100%;box-sizing:border-box"></textarea>
|
||||
<div class="form-row" style="margin-top:8px;gap:6px">
|
||||
<button type="button" class="btn" id="or-save-btn">保存复盘记录</button>
|
||||
<button type="button" class="btn-secondary" id="or-clear-btn">清空表单</button>
|
||||
<button type="button" class="btn-secondary" id="or-del-btn">删除复盘</button>
|
||||
<button type="button" class="btn-secondary" id="or-clear-btn">取消</button>
|
||||
<button type="button" class="btn-secondary" id="or-del-btn">删除复盘内容</button>
|
||||
<span class="muted" id="or-save-status"></span>
|
||||
</div>
|
||||
<div id="or-legs-host" style="margin-top:10px;font-size:.74rem"></div>
|
||||
@@ -148,35 +176,45 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# 3. 已复盘记录 + 详情 #}
|
||||
<div class="card" style="margin-bottom:10px">
|
||||
<h3 style="margin-top:0">统计</h3>
|
||||
<div id="or-kpi" class="form-row" style="flex-wrap:wrap;gap:10px"></div>
|
||||
<div id="or-stats-groups" style="margin-top:10px;display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:8px"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3 style="margin-top:0" id="or-list-title">期权交易记录</h3>
|
||||
<p class="muted" style="margin-top:0;font-size:.72rem">点选一行,上方复盘表单自动填入.</p>
|
||||
<h3>复盘记录</h3>
|
||||
<p class="muted" style="margin-top:0;font-size:.72rem">保存后出现在此.点一行查看详情(含截图与完整复盘内容).</p>
|
||||
<div class="options-strike-table-wrap">
|
||||
<table class="options-strike-table or-trades-table" id="or-trades-table">
|
||||
<table class="options-strike-table or-reviewed-table" id="or-reviewed-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>类型</th>
|
||||
<th>标的/合约</th>
|
||||
<th>盈亏</th>
|
||||
<th>开/平</th>
|
||||
<th>持有</th>
|
||||
<th>策略</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
<th>结果</th>
|
||||
<th>复盘时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="or-trades-tbody">
|
||||
<tr><td colspan="8" class="muted">加载中…</td></tr>
|
||||
<tbody id="or-reviewed-tbody">
|
||||
<tr><td colspan="6" class="muted">加载中…</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="or-detail-panel hidden" id="or-detail-panel">
|
||||
<div class="form-row" style="align-items:center;gap:8px;margin-bottom:6px">
|
||||
<h3 style="margin:0;margin-right:auto" id="or-detail-title">复盘详情</h3>
|
||||
<button type="button" class="btn-secondary" id="or-detail-edit-btn" style="font-size:.72rem;padding:2px 8px">编辑</button>
|
||||
<button type="button" class="btn-secondary" id="or-detail-close-btn" style="font-size:.72rem;padding:2px 8px">收起</button>
|
||||
</div>
|
||||
<div class="or-detail-grid" id="or-detail-meta"></div>
|
||||
<div id="or-detail-text" style="font-size:.76rem;line-height:1.5;margin-bottom:8px"></div>
|
||||
<div class="or-detail-images" id="or-detail-images"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# 4. 统计 #}
|
||||
<div class="card" style="margin-bottom:10px">
|
||||
<h3>统计</h3>
|
||||
<div id="or-kpi" class="form-row" style="flex-wrap:wrap;gap:10px"></div>
|
||||
<div id="or-stats-groups" style="margin-top:10px;display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:8px"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/options_review.js?v=7"></script>
|
||||
<script src="/static/options_review.js?v=8"></script>
|
||||
|
||||
Reference in New Issue
Block a user