/** * 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, "&") .replace(//g, ">") .replace(/"/g, """); } function fmtPnl(v) { if (v == null || v === "") return "—"; var n = Number(v); if (Number.isNaN(n)) return "—"; return (n >= 0 ? "+" : "") + n.toFixed(2) + "U"; } 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 closeReasonLabel(r) { var map = { perp_tp: "永续止盈", perp_sl: "永续止损", oo_expiry_loss: "期期到期亏损", oo_expiry_win: "期期到期盈利", target_win_leg: "期期平盈利腿", target_up_win_leg: "期期上破·平盈利腿", target_down_win_leg: "期期下破·平盈利腿", profit_rr_win_leg: "期期盈亏比达标·平盈利腿", oo_rest_closing: "期期残值平·清亏损腿中", oo_rest_closed: "期期残值平·两腿已平", orphaned_after_tp: "止盈后持有至到期", orphaned_option_expiry: "残腿到期", hold_to_expiry: "持有至到期", expiry: "到期", manual: "人工结束", partial_fail: "半腿失败", cancelled: "已取消", tp: "止盈", sl: "止损", }; var key = String(r || "").trim(); if (!key) return "—"; return map[key] || key; } function legRoleLabel(role) { var map = { perp: "永续腿", option_hedge: "保险期权", option_a: "期期腿A", option_b: "期期腿B", }; var key = String(role || "").trim(); if (!key) return "—"; return map[key] || key; } 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 ? " · " + closeReasonLabel(t.plan_close_reason) : "") ); } function pnlClass(v) { var n = Number(v); if (n > 0) return "pos-pnl-profit"; if (n < 0) return "pos-pnl-loss"; return ""; } function resultClass(tag) { var t = String(tag || "").trim(); if (t === "盈利") return "pos-pnl-profit"; if (t === "亏损") return "pos-pnl-loss"; return ""; } function tradeContractLabel(t) { if (!t) return "—"; if (t.source_type === "option_spot") return t.inst_id || t.underlying || "—"; return t.underlying || "—"; } 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 q = (($("or-filter-q") || $("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 (q) p.set("q", q); 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 tradeModeFromDom() { var tabs = document.querySelector(".or-tabs"); return (tabs && tabs.getAttribute("data-okx-trade-mode")) || "options"; } function defaultSourceForMode(mode) { if (mode === "options_options") return "options_options"; if (mode === "perp_options") return "perp_options"; return "option_spot"; } function setActiveTab(source) { var mode = tradeModeFromDom(); var allowed = defaultSourceForMode(mode); activeSource = source || allowed; if (activeSource !== allowed) activeSource = allowed; 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 = '加载中…'; } var p = baseQs(); // 交易记录保留已复盘条目,不再只显示待复盘 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 = '加载失败'; 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 = '暂无交易记录'; endListLoad(wrap); return; } tbody.innerHTML = rows .map(function (t) { tradesCache[t.id] = t; var active = currentTradeId === t.id ? " or-row-active" : ""; var reviewed = !!t.reviewed; var actionBtn = reviewed ? '' : ''; var badgeExtra = reviewed ? ' 已复盘' : ""; return ( '' + "" + escapeHtml(t.source_label || t.source_type) + "" + badgeExtra + "" + "" + escapeHtml(tradeTitle(t)) + "" + '' + fmtPnl(t.realized_pnl_total) + "" + '' + escapeHtml(t.opened_at || "—") + "" + '' + escapeHtml(t.closed_at || "—") + "" + "" + fmtHold(t.hold_seconds) + "" + "" + actionBtn + " " + '' + "" ); }) .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 = '加载失败'; 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 = '加载中…'; } 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 = '加载失败'; 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 = '暂无复盘记录'; endListLoad(wrap); return; } tbody.innerHTML = rows .map(function (t) { reviewedCache[t.id] = t; var entry = t.entry || {}; var direction = t.direction_view || entry.direction_view || ""; var entryLogic = t.entry_logic || entry.entry_logic || ""; return ( '' + "" + escapeHtml(t.source_label || t.source_type) + "" + "" + escapeHtml(tradeContractLabel(t)) + "" + "" + escapeHtml(direction || "—") + "" + '' + fmtPnl(t.realized_pnl_total) + "" + '' + escapeHtml(t.opened_at || "—") + "" + '' + escapeHtml(t.closed_at || "—") + "" + "" + escapeHtml(fmtHold(t.hold_seconds)) + "" + "" + escapeHtml(t.strategy_tag || "—") + "" + "" + escapeHtml(entryLogic || "—") + "" + '' + 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"))); }); }); endListLoad(wrap); }) .catch(function () { tbody.innerHTML = '加载失败'; endListLoad(wrap); }); } function hideLightbox() { var box = $("or-img-lightbox"); if (box) box.hidden = true; var img = $("or-img-lightbox-img"); if (img) img.src = ""; } function showLightbox(src) { var url = String(src || "").trim(); if (!url) return; var box = $("or-img-lightbox"); var img = $("or-img-lightbox-img"); if (box && img) { img.src = url; box.hidden = false; return; } if (typeof global.showImage === "function") { global.showImage(url); } else if (typeof window.showImage === "function") { window.showImage(url); } else { global.open(url, "_blank"); } } function hideDetail() { hideLightbox(); var backdrop = $("or-detail-backdrop"); if (backdrop) backdrop.hidden = true; } function openDetail(tradeId) { var backdrop = $("or-detail-backdrop"); if (!backdrop) return; backdrop.hidden = false; ($("or-detail-title") || {}).textContent = "加载中…"; ($("or-detail-meta") || {}).innerHTML = ""; ($("or-detail-text") || {}).innerHTML = ""; ($("or-detail-images") || {}).innerHTML = ""; 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 optionsJournalImgSrc(file) { var name = String(file || "").trim().replace(/\\/g, "/"); var slash = name.lastIndexOf("/"); if (slash >= 0) name = name.slice(slash + 1); if (!name) return ""; // options_journal_* 在子目录;误走合约上传的 journal_* 在 static/images 根目录 var base = name.toLowerCase().indexOf("options_journal_") === 0 ? "/static/images/options_journal/" : "/static/images/"; return base + encodeURIComponent(name); } function renderDetailImages(images) { var imagesHost = $("or-detail-images"); if (!imagesHost) return; var byTf = {}; (images || []).forEach(function (img) { var tf = String((img && img.tf) || "").trim(); var file = String((img && img.file) || "").trim(); if (!file) return; var key = tf || "_"; byTf[key] = file; }); var order = ["5m", "15m", "1h", "4h"]; var keys = order.slice(); Object.keys(byTf).forEach(function (k) { if (keys.indexOf(k) < 0) keys.push(k); }); var cells = keys .map(function (tf) { var file = byTf[tf]; if (!file) { if (order.indexOf(tf) < 0) return ""; return ( '
' + '' + escapeHtml(tf) + "" + '
未上传
' + "
" ); } var src = optionsJournalImgSrc(file); var label = escapeHtml(tf === "_" ? "截图" : tf); return ( '
' + '' + label + "" + '' +
          label +
          '' + "
" ); }) .filter(Boolean); if (!cells.length) { imagesHost.innerHTML = '
无截图
'; return; } imagesHost.innerHTML = cells.join(""); imagesHost.querySelectorAll("img").forEach(function (img) { img.addEventListener("error", function () { var cell = img.closest(".or-detail-img-cell"); if (!cell) return; var label = cell.querySelector(".or-detail-img-label"); var tf = label ? label.textContent : "截图"; cell.innerHTML = '' + escapeHtml(tf) + "" + '
文件缺失或无法加载
'; }); img.addEventListener("click", function () { var src = img.getAttribute("data-src") || img.src; showLightbox(src); }); }); } 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) { var cls = ""; if (pair[0] === "盈亏" || pair[0] === "永续盈亏" || pair[0] === "期权盈亏") { cls = pnlClass(t.realized_pnl_total); if (pair[0] === "永续盈亏") cls = pnlClass(t.realized_pnl_perp); if (pair[0] === "期权盈亏") cls = pnlClass(t.realized_pnl_options); } else if (pair[0] === "结果") { cls = resultClass(e.result_tag); } 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(legRoleLabel(leg.leg_role)) + "" + escapeHtml(leg.inst_id || leg.symbol || "") + "" + fmtPnl(leg.realized_pnl) + "" + escapeHtml(closeReasonLabel(leg.close_reason)) + "
" ); } text.innerHTML = lines.join("") || '
无额外备注
'; } var imagesHost = $("or-detail-images"); if (imagesHost) { renderDetailImages(e.images || []); } } function renderGroup(title, items) { if (!items || !items.length) return ""; var lines = items .slice(0, 8) .map(function (g) { var keyLabel = title === "对冲结束原因" ? closeReasonLabel(g.key) : String(g.key || ""); return ( '
' + '' + escapeHtml(keyLabel) + " · " + g.count + "笔" + '' + fmtPnl(g.pnl_sum) + " / 胜" + (g.win_rate || 0) + "%" + "
" ); }) .join(""); return ( '
' + title + "
" + lines + "
" ); } 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) { var cls = ""; if (pair[0] === "累计盈亏") cls = pnlClass(k.pnl_sum); if (pair[0] === "平均盈亏") cls = pnlClass(k.avg_pnl); return ( '
' + pair[0] + '
' + pair[1] + "
" ); }) .join(""); var html = [ 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), ] .filter(Boolean) .join(""); groups.innerHTML = html || '
暂无分组数据
'; }) .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 ? " · " + closeReasonLabel(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 || closeReasonLabel(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) { var src = optionsJournalImgSrc(img.file); status.innerHTML = '已有 ' + escapeHtml(img.file) + '
' +
            escapeHtml(img.tf || '; status.className = "journal-upload-status or-upload-status journal-upload-status--ok"; } } }); var legsHost = $("or-legs-host"); if (legsHost) { if (t.legs && t.legs.length) { legsHost.innerHTML = "

计划腿

" + t.legs .map(function (leg) { return ( "" ); }) .join("") + "
合约盈亏原因
" + escapeHtml(legRoleLabel(leg.leg_role)) + "" + escapeHtml(leg.inst_id || leg.symbol || "") + "" + fmtPnl(leg.realized_pnl) + "" + escapeHtml(closeReasonLabel(leg.close_reason)) + "
"; } 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) { hideDetail(); openJournalForm(id); } }); } var detailBackdrop = $("or-detail-backdrop"); if (detailBackdrop) { detailBackdrop.addEventListener("click", function (ev) { if (ev.target === detailBackdrop) hideDetail(); }); } var lightbox = $("or-img-lightbox"); if (lightbox) { lightbox.addEventListener("click", function () { hideLightbox(); }); } document.addEventListener("keydown", function (ev) { if (ev.key !== "Escape") return; var lb = $("or-img-lightbox"); if (lb && !lb.hidden) { hideLightbox(); return; } var bd = $("or-detail-backdrop"); if (bd && !bd.hidden) hideDetail(); }); ["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-q", "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(); hardenSearchAutofill(); setActiveTab(defaultSourceForMode(tradeModeFromDom())); } function hardenSearchAutofill() { var qEl = $("or-filter-q"); if (!qEl) return; function wipe() { qEl.value = ""; } wipe(); qEl.addEventListener("focus", function () { qEl.removeAttribute("readonly"); }); qEl.addEventListener("blur", function () { if (!qEl.value) qEl.setAttribute("readonly", "readonly"); }); // 密码管理器常延后写入用户名,加载后再清两次 setTimeout(wipe, 200); setTimeout(wipe, 800); } global.OptionsReview = { init: init, openJournalForm: openJournalForm, hideJournalForm: hideJournalForm, }; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } })(typeof window !== "undefined" ? window : globalThis);