Files
crypto_monitor/lib/common/static/options_review.js
T
dekun 57f8d6761c Keep options review paging in-card without layout jump.
Soft-load table rows on prev/next so height stays stable and the page below does not flash.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 11:40:23 +08:00

1112 lines
35 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* OKX 期权复盘:待复盘交易(5行) → 点复盘出表单 → 复盘记录详情 → 统计.
*/
(function (global) {
"use strict";
var PAGE_SIZE = 5;
var TAB_LABELS = {
option_spot: "期权交易记录",
options_options: "期期对冲记录",
perp_options: "永期对冲记录",
};
var FORM_PRESETS = {
option_spot: {
strategy: ["顺势", "反转"],
direction: ["多", "空"],
entry: ["假突破", "结构突破"],
},
hedge: {
strategy: ["横盘", "趋势"],
direction: ["多", "空"],
entry: ["横盘博弈方向", "趋势对冲止损"],
},
};
var RESULT_OPTIONS = ["盈利", "亏损", "持平"];
var activeSource = "option_spot";
var currentTradeId = null;
var draftId = "";
var tradesCache = {};
var reviewedCache = {};
var tradesPage = 0;
var tradesPages = 1;
var reviewedPage = 0;
var reviewedPages = 1;
function $(id) {
return document.getElementById(id);
}
function escapeHtml(s) {
return String(s == null ? "" : s)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function fmtPnl(v) {
if (v == null || v === "") return "—";
var n = Number(v);
if (Number.isNaN(n)) return "—";
return (n >= 0 ? "+" : "") + n.toFixed(2);
}
function fmtHold(sec) {
if (sec == null) return "—";
var s = Math.max(0, Number(sec) || 0);
if (s < 3600) return Math.round(s / 60) + "m";
if (s < 86400) return (s / 3600).toFixed(1) + "h";
return (s / 86400).toFixed(1) + "d";
}
function toLocalInput(ts) {
if (!ts) return "";
var s = String(ts).trim().replace(" ", "T");
if (s.length >= 16) return s.slice(0, 16);
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, "");
}
var s = "";
for (var i = 0; i < 32; i++) s += Math.floor(Math.random() * 16).toString(16);
return s;
}
function baseQs() {
var p = new URLSearchParams();
p.set("source_type", activeSource);
var uly = ($("or-filter-uly") || {}).value || "";
var opt = ($("or-filter-opt") || {}).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 (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;
}
function setSyncStatus(text) {
var el = $("or-sync-status");
if (el) el.textContent = text || "";
}
function reloadAll() {
setSyncStatus("读取本地记录…");
loadTrades({ sync: true });
loadReviewed({ sync: false });
loadStats();
}
function isHedgeSource(sourceType) {
return sourceType === "options_options" || sourceType === "perp_options";
}
function fillSelect(el, options, placeholder) {
if (!el) return;
var keep = el.value;
el.innerHTML = "";
var first = document.createElement("option");
first.value = "";
first.textContent = placeholder || "";
el.appendChild(first);
(options || []).forEach(function (v) {
var opt = document.createElement("option");
opt.value = v;
opt.textContent = v;
el.appendChild(opt);
});
if (keep) setSelectValue(el, keep);
}
function setSelectValue(el, value) {
if (!el) return;
var v = value == null ? "" : String(value);
if (!v) {
el.value = "";
return;
}
var found = false;
for (var i = 0; i < el.options.length; i++) {
if (el.options[i].value === v) {
found = true;
break;
}
}
if (!found) {
var opt = document.createElement("option");
opt.value = v;
opt.textContent = v;
el.appendChild(opt);
}
el.value = v;
}
function applyFormPresets(sourceType) {
var preset = isHedgeSource(sourceType) ? FORM_PRESETS.hedge : FORM_PRESETS.option_spot;
fillSelect($("or-f-strategy"), preset.strategy, "策略标签");
fillSelect($("or-f-direction"), preset.direction, "方向判断");
fillSelect($("or-f-entry"), preset.entry, "入场逻辑");
fillSelect($("or-f-result"), RESULT_OPTIONS, "结果标签");
}
function autoDirection(t) {
if (!t) return "";
if (isHedgeSource(t.source_type)) {
var d = String(t.direction || "").trim().toLowerCase();
if (d === "long" || d === "buy" || d === "多") return "多";
if (d === "short" || d === "sell" || d === "空") return "空";
return "";
}
var ot = String(t.opt_type || "").trim().toUpperCase();
if (ot === "C" || ot === "CALL") return "多";
if (ot === "P" || ot === "PUT") return "空";
return "";
}
function autoResultTag(pnl) {
if (pnl == null || pnl === "") return "";
var n = Number(pnl);
if (Number.isNaN(n)) return "";
if (n > 0) return "盈利";
if (n < 0) return "亏损";
return "持平";
}
function setActiveTab(source) {
activeSource = source || "option_spot";
tradesPage = 0;
reviewedPage = 0;
document.querySelectorAll(".or-tab").forEach(function (btn) {
btn.classList.toggle("active", btn.getAttribute("data-source") === activeSource);
});
var title = $("or-list-title");
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) + " / " + tradesPages + " 页";
}
if (prev) prev.disabled = tradesPage <= 0;
if (next) next.disabled = tradesPage + 1 >= tradesPages;
}
function updateReviewedPager() {
var label = $("or-reviewed-page-label");
var prev = $("or-reviewed-prev");
var next = $("or-reviewed-next");
if (label) {
label.textContent = "第 " + (reviewedPage + 1) + " / " + reviewedPages + " 页";
}
if (prev) prev.disabled = reviewedPage <= 0;
if (next) next.disabled = reviewedPage + 1 >= reviewedPages;
}
function applyPagerMeta(data, kind) {
var pages = Number(data.pages || 1);
if (!pages || pages < 1) pages = 1;
var clamped = false;
if (kind === "trades") {
tradesPages = pages;
if (tradesPage >= tradesPages) {
tradesPage = Math.max(0, tradesPages - 1);
clamped = true;
}
updateTradesPager();
} else {
reviewedPages = pages;
if (reviewedPage >= reviewedPages) {
reviewedPage = Math.max(0, reviewedPages - 1);
clamped = true;
}
updateReviewedPager();
}
return clamped;
}
function beginListLoad(wrapId, soft) {
var wrap = $(wrapId);
if (!wrap) return null;
if (soft) {
if (!wrap.style.minHeight) {
wrap.style.minHeight = Math.max(wrap.offsetHeight, 1) + "px";
}
wrap.classList.add("or-list-loading");
} else {
wrap.classList.remove("or-list-loading");
wrap.style.minHeight = "";
}
return wrap;
}
function endListLoad(wrap) {
if (!wrap) return;
wrap.classList.remove("or-list-loading");
wrap.style.minHeight = "";
}
function loadTrades(opts) {
opts = opts || {};
var doSync = opts.sync !== false;
var soft = !!opts.soft;
var tbody = $("or-trades-tbody");
if (!tbody) return;
var wrap = beginListLoad("or-trades-wrap", soft);
if (!soft) {
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));
if (!doSync) p.set("sync", "0");
fetch("/api/options/review/trades?" + p.toString(), { credentials: "same-origin" })
.then(function (r) {
return r.json();
})
.then(function (data) {
if (doSync) setSyncStatus("本地记录已加载");
if (!data.ok) {
tbody.innerHTML = '<tr><td colspan="6" class="muted">加载失败</td></tr>';
endListLoad(wrap);
return;
}
if (applyPagerMeta(data, "trades") && Number(data.total || 0) > 0) {
loadTrades(opts);
return;
}
var rows = data.trades || [];
tradesCache = {};
if (!rows.length) {
tbody.innerHTML =
'<tr><td colspan="6" class="muted">暂无待复盘记录</td></tr>';
endListLoad(wrap);
return;
}
tbody.innerHTML = rows
.map(function (t) {
tradesCache[t.id] = t;
var active = currentTradeId === t.id ? " or-row-active" : "";
return (
'<tr class="or-trade-row' +
active +
'" 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 class="muted" style="font-size:12px">' +
escapeHtml(t.opened_at || "—") +
"<br>" +
escapeHtml(t.closed_at || "—") +
"</td>" +
"<td>" +
fmtHold(t.hold_seconds) +
"</td>" +
'<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-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) {
btn.addEventListener("click", function (ev) {
ev.preventDefault();
ev.stopPropagation();
hideTrade(Number(btn.getAttribute("data-id")));
});
});
endListLoad(wrap);
})
.catch(function () {
tbody.innerHTML = '<tr><td colspan="6" class="muted">加载失败</td></tr>';
endListLoad(wrap);
});
}
function loadReviewed(opts) {
opts = opts || {};
var doSync = opts.sync === true;
var soft = !!opts.soft;
var tbody = $("or-reviewed-tbody");
if (!tbody) return;
var wrap = beginListLoad("or-reviewed-wrap", soft);
if (!soft) {
tbody.innerHTML = '<tr><td colspan="6" class="muted">加载中…</td></tr>';
}
var p = baseQs();
p.set("reviewed", "1");
p.set("limit", String(PAGE_SIZE));
p.set("offset", String(reviewedPage * PAGE_SIZE));
if (!doSync) p.set("sync", "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>';
endListLoad(wrap);
return;
}
if (applyPagerMeta(data, "reviewed") && Number(data.total || 0) > 0) {
loadReviewed(opts);
return;
}
var rows = data.trades || [];
reviewedCache = {};
if (!rows.length) {
tbody.innerHTML = '<tr><td colspan="6" class="muted">暂无复盘记录</td></tr>';
endListLoad(wrap);
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")));
});
});
endListLoad(wrap);
})
.catch(function () {
tbody.innerHTML = '<tr><td colspan="6" class="muted">加载失败</td></tr>';
endListLoad(wrap);
});
}
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 (
'<div class="or-stat-card"><div class="muted">' +
title +
'</div><div class="muted">无数据</div></div>'
);
}
var lines = items
.slice(0, 8)
.map(function (g) {
return (
'<div style="display:flex;justify-content:space-between;gap:8px;font-size:13px">' +
"<span>" +
escapeHtml(g.key) +
" · " +
g.count +
"笔</span>" +
"<span>" +
fmtPnl(g.pnl_sum) +
" / 胜" +
(g.win_rate || 0) +
"%</span>" +
"</div>"
);
})
.join("");
return (
'<div class="or-stat-card"><div style="font-weight:600;margin-bottom:6px">' +
title +
"</div>" +
lines +
"</div>"
);
}
function loadStats() {
var kpi = $("or-kpi");
var groups = $("or-stats-groups");
if (!kpi || !groups) return;
fetch("/api/options/review/stats?" + baseQs().toString(), { credentials: "same-origin" })
.then(function (r) {
return r.json();
})
.then(function (data) {
if (!data.ok) return;
var k = data.kpi || {};
kpi.innerHTML = [
["笔数", k.total],
["已复盘率", (k.review_rate || 0) + "%"],
["胜率", (k.win_rate || 0) + "%"],
["累计盈亏", fmtPnl(k.pnl_sum)],
["平均盈亏", fmtPnl(k.avg_pnl)],
["平均持有", fmtHold(k.avg_hold_sec)],
]
.map(function (pair) {
return (
'<div><div class="muted" style="font-size:12px">' +
pair[0] +
'</div><div style="font-weight:600">' +
pair[1] +
"</div></div>"
);
})
.join("");
groups.innerHTML = [
renderGroup("按类型", data.by_source_type),
renderGroup("按标的", data.by_underlying),
renderGroup("按策略", data.by_strategy),
renderGroup("对冲结束原因", data.by_close_reason),
renderGroup("持有周期", data.by_hold_bucket),
renderGroup("Call/Put", data.by_opt_type),
].join("");
})
.catch(function () {});
}
function resetUploadSlots() {
draftId = newDraftId();
var draftEl = $("or-draft-id");
if (draftEl) draftEl.value = draftId;
document.querySelectorAll("#or-upload-slots .or-upload-hidden").forEach(function (el) {
el.value = "";
});
document.querySelectorAll("#or-upload-slots .or-upload-input").forEach(function (el) {
el.value = "";
});
document.querySelectorAll("#or-upload-slots .or-upload-status").forEach(function (el) {
el.textContent = "";
});
}
function bindUploadSlots() {
document.querySelectorAll("#or-upload-slots .or-upload-input").forEach(function (input) {
if (input.dataset.orBound === "1") return;
input.dataset.orBound = "1";
input.addEventListener("change", function () {
var file = input.files && input.files[0];
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) {
if (hidden) hidden.value = "";
if (status) {
status.textContent = "";
status.className = "journal-upload-status or-upload-status";
}
return;
}
if (!draftId) draftId = newDraftId();
if (status) {
status.textContent = "上传中…";
status.className = "journal-upload-status or-upload-status journal-upload-status--pending";
}
var fd = new FormData();
fd.append("draft_id", draftId);
fd.append("tf", input.getAttribute("data-tf") || "");
fd.append("file", file);
fetch("/api/options/review/upload_slot", {
method: "POST",
body: fd,
credentials: "same-origin",
})
.then(function (r) {
return r.json();
})
.then(function (data) {
if (!data.ok) throw new Error(data.error || "fail");
if (hidden) hidden.value = data.file;
if (status) {
status.textContent = "上传成功 " + data.file;
status.className = "journal-upload-status or-upload-status journal-upload-status--ok";
}
input.value = "";
})
.catch(function () {
if (hidden) hidden.value = "";
if (status) {
status.textContent = "上传失败";
status.className = "journal-upload-status or-upload-status journal-upload-status--err";
}
});
});
});
}
function setMoodTags(raw) {
var set = {};
String(raw || "")
.split(/[,]/)
.map(function (x) {
return x.trim();
})
.filter(Boolean)
.forEach(function (x) {
set[x] = true;
});
document.querySelectorAll(".or-mood").forEach(function (cb) {
cb.checked = !!set[cb.value];
});
}
function collectMoodTags() {
var out = [];
document.querySelectorAll(".or-mood:checked").forEach(function (cb) {
out.push(cb.value);
});
return out.join(",");
}
function collectImages() {
var out = [];
document.querySelectorAll("#or-upload-slots .or-upload-hidden").forEach(function (el) {
var file = (el.value || "").trim();
if (file) out.push({ tf: el.getAttribute("data-tf") || "", file: file });
});
return out;
}
function hideJournalForm() {
currentTradeId = null;
var card = $("or-journal-card");
if (card) card.classList.add("hidden");
document.querySelectorAll(".or-trade-row").forEach(function (tr) {
tr.classList.remove("or-row-active");
});
($("or-trade-id") || {}).value = "";
($("or-f-open") || {}).value = "";
($("or-f-close") || {}).value = "";
($("or-f-coin") || {}).value = "";
($("or-f-inst") || {}).value = "";
($("or-f-pnl") || {}).value = "";
($("or-f-hold") || {}).value = "";
($("or-f-strategy") || {}).value = "";
($("or-f-direction") || {}).value = "";
($("or-f-exit") || {}).value = "";
($("or-f-followed") || {}).value = "";
($("or-f-result") || {}).value = "";
($("or-f-entry") || {}).value = "";
($("or-f-note") || {}).value = "";
setMoodTags("");
resetUploadSlots();
var summary = $("or-journal-summary");
if (summary) {
summary.textContent = "截图槽位与合约复盘相同(5m / 15m / 1h / 4h).";
}
var legsHost = $("or-legs-host");
if (legsHost) legsHost.innerHTML = "";
($("or-save-status") || {}).textContent = "";
}
function openJournalForm(tradeId) {
currentTradeId = tradeId;
var card = $("or-journal-card");
if (!card) return;
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);
});
resetUploadSlots();
bindUploadSlots();
($("or-save-status") || {}).textContent = "加载中…";
fetch("/api/options/review/trades/" + tradeId, { credentials: "same-origin" })
.then(function (r) {
return r.json();
})
.then(function (data) {
if (!data.ok || !data.trade) {
($("or-save-status") || {}).textContent = "加载失败";
return;
}
fillForm(data.trade);
($("or-save-status") || {}).textContent = "已选中 #" + tradeId;
})
.catch(function () {
($("or-save-status") || {}).textContent = "加载失败";
});
}
function fillForm(t) {
var e = t.entry || {};
applyFormPresets(t.source_type || activeSource);
($("or-trade-id") || {}).value = String(t.id || "");
($("or-f-open") || {}).value = toLocalInput(t.opened_at);
($("or-f-close") || {}).value = toLocalInput(t.closed_at);
($("or-f-coin") || {}).value = t.underlying || "";
($("or-f-inst") || {}).value =
t.source_type === "option_spot"
? t.inst_id || ""
: (t.source_label || "") + (t.plan_close_reason ? " · " + t.plan_close_reason : "");
($("or-f-pnl") || {}).value = fmtPnl(t.realized_pnl_total);
($("or-f-hold") || {}).value = fmtHold(t.hold_seconds);
setSelectValue($("or-f-strategy"), e.strategy_tag || "");
setSelectValue($("or-f-direction"), e.direction_view || autoDirection(t));
($("or-f-exit") || {}).value = e.exit_reason || t.plan_close_reason || "";
($("or-f-followed") || {}).value = e.followed_plan || "";
setSelectValue($("or-f-result"), e.result_tag || autoResultTag(t.realized_pnl_total));
setSelectValue($("or-f-entry"), e.entry_logic || "");
($("or-f-note") || {}).value = e.note || "";
setMoodTags(e.mistake_tags);
var summary = $("or-journal-summary");
if (summary) {
summary.textContent =
(t.source_label || "") +
" · " +
(t.inst_id || t.underlying || "#" + t.id) +
" · 盈亏 " +
fmtPnl(t.realized_pnl_total) +
(t.is_hedge
? " (永续 " + fmtPnl(t.realized_pnl_perp) + " / 期权 " + fmtPnl(t.realized_pnl_options) + ")"
: "");
}
(e.images || []).forEach(function (img) {
var hidden = document.querySelector(
'#or-upload-slots .or-upload-hidden[data-tf="' + img.tf + '"]'
);
var status = document.querySelector(
'#or-upload-slots .or-upload-status[data-tf="' + img.tf + '"]'
);
if (hidden && img.file) {
hidden.value = img.file;
if (status) status.textContent = "已有 " + img.file;
}
});
var legsHost = $("or-legs-host");
if (legsHost) {
if (t.legs && t.legs.length) {
legsHost.innerHTML =
"<h3>计划腿</h3><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>";
} else {
legsHost.innerHTML = "";
}
}
}
function hideTrade(tradeId) {
if (!tradeId) return;
if (!confirm("从待复盘列表删除并隐藏?刷新后也不会再出现.")) return;
fetch("/api/options/review/trades/" + tradeId, {
method: "DELETE",
credentials: "same-origin",
})
.then(function (r) {
return r.json();
})
.then(function (data) {
if (!data.ok) {
alert(data.msg || "删除失败");
return;
}
if (currentTradeId === tradeId) hideJournalForm();
reloadAll();
})
.catch(function () {
alert("删除失败");
});
}
function saveEntry() {
var tradeId = Number(($("or-trade-id") || {}).value || 0);
if (!tradeId) {
alert("请先点击交易记录中的「复盘」");
return;
}
var strategy = (($("or-f-strategy") || {}).value || "").trim();
if (!strategy) {
alert("请选择策略标签");
return;
}
var payload = {
trade_id: tradeId,
strategy_tag: strategy,
direction_view: ($("or-f-direction") || {}).value || "",
exit_reason: ($("or-f-exit") || {}).value || "",
followed_plan: ($("or-f-followed") || {}).value || "",
result_tag: ($("or-f-result") || {}).value || "",
mistake_tags: collectMoodTags(),
entry_logic: ($("or-f-entry") || {}).value || "",
note: ($("or-f-note") || {}).value || "",
images: collectImages(),
};
($("or-save-status") || {}).textContent = "保存中…";
fetch("/api/options/review/entry", {
method: "POST",
credentials: "same-origin",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
})
.then(function (r) {
return r.json();
})
.then(function (data) {
if (!data.ok) {
($("or-save-status") || {}).textContent = data.msg || "保存失败";
return;
}
($("or-save-status") || {}).textContent = "已保存";
hideJournalForm();
reloadAll();
openDetail(tradeId);
})
.catch(function () {
($("or-save-status") || {}).textContent = "保存失败";
});
}
function deleteEntry() {
var tradeId = Number(($("or-trade-id") || {}).value || 0);
if (!tradeId) return;
if (!confirm("删除该条复盘内容与图片?交易记录会回到待复盘列表.")) return;
fetch("/api/options/review/entry/" + tradeId, {
method: "DELETE",
credentials: "same-origin",
})
.then(function (r) {
return r.json();
})
.then(function () {
hideJournalForm();
hideDetail();
reloadAll();
});
}
function init() {
if (!$("options-review-root")) return;
document.querySelectorAll(".or-tab").forEach(function (btn) {
btn.addEventListener("click", function () {
setActiveTab(btn.getAttribute("data-source"));
});
});
var reloadBtn = $("or-reload-btn");
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 reviewedPrev = $("or-reviewed-prev");
var reviewedNext = $("or-reviewed-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);
if (prevBtn) {
prevBtn.addEventListener("click", function (ev) {
ev.preventDefault();
ev.stopPropagation();
if (tradesPage <= 0) return;
tradesPage -= 1;
updateTradesPager();
loadTrades({ sync: false, soft: true });
});
}
if (nextBtn) {
nextBtn.addEventListener("click", function (ev) {
ev.preventDefault();
ev.stopPropagation();
if (tradesPage + 1 >= tradesPages) return;
tradesPage += 1;
updateTradesPager();
loadTrades({ sync: false, soft: true });
});
}
if (reviewedPrev) {
reviewedPrev.addEventListener("click", function (ev) {
ev.preventDefault();
ev.stopPropagation();
if (reviewedPage <= 0) return;
reviewedPage -= 1;
updateReviewedPager();
loadReviewed({ sync: false, soft: true });
});
}
if (reviewedNext) {
reviewedNext.addEventListener("click", function (ev) {
ev.preventDefault();
ev.stopPropagation();
if (reviewedPage + 1 >= reviewedPages) return;
reviewedPage += 1;
updateReviewedPager();
loadReviewed({ sync: false, soft: true });
});
}
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;
reviewedPage = 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;
reviewedPage = 0;
reloadAll();
});
}
});
bindUploadSlots();
hideJournalForm();
hideDetail();
setActiveTab("option_spot");
}
global.OptionsReview = {
init: init,
openJournalForm: openJournalForm,
hideJournalForm: hideJournalForm,
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})(typeof window !== "undefined" ? window : globalThis);