/** * OKX 期权复盘(含对冲):列表 / 同步 / 统计 / 编辑上传. */ (function (global) { "use strict"; var SLOT_TFS = ["chart", "entry", "exit", "other"]; var SLOT_LABELS = { chart: "走势图", entry: "入场", exit: "离场", other: "其他" }; var currentTradeId = null; var draftId = ""; function $(id) { return document.getElementById(id); } function fmtPnl(v) { if (v == null || v === "") return "—"; var n = Number(v); if (Number.isNaN(n)) return "—"; var s = (n >= 0 ? "+" : "") + n.toFixed(2); return s; } 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 qs() { var p = new URLSearchParams(); var source = ($("or-filter-source") || {}).value || ""; 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 (source) p.set("source_type", source); 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(); } 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 setSyncStatus(text) { var el = $("or-sync-status"); if (el) el.textContent = text || ""; } function syncNow() { setSyncStatus("同步中…"); fetch("/api/options/review/sync", { method: "POST", credentials: "same-origin" }) .then(function (r) { return r.json(); }) .then(function (data) { var parts = []; if (data.options) { parts.push( data.options.ok ? "期权 +" + (data.options.inserted || 0) + "/更" + (data.options.updated || 0) : "期权:" + (data.options.msg || "失败") ); } if (data.hedge) { parts.push( data.hedge.ok ? "对冲 +" + (data.hedge.inserted || 0) + "/更" + (data.hedge.updated || 0) : "对冲失败" ); } setSyncStatus(parts.join(" · ") || "完成"); reloadAll(); }) .catch(function () { setSyncStatus("同步失败"); }); } function reloadAll() { loadTrades(); loadStats(); } function loadTrades() { var tbody = $("or-trades-tbody"); if (!tbody) return; tbody.innerHTML = '加载中…'; fetch("/api/options/review/trades?" + qs(), { credentials: "same-origin" }) .then(function (r) { return r.json(); }) .then(function (data) { if (!data.ok) { tbody.innerHTML = '加载失败'; return; } var rows = data.trades || []; if (!rows.length) { tbody.innerHTML = '暂无记录,请先同步'; return; } tbody.innerHTML = rows .map(function (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" : ""; return ( "" + "" + (t.source_label || t.source_type) + "" + "" + title + "" + "" + fmtPnl(t.realized_pnl_total) + "" + "" + (t.opened_at || "—") + "
" + (t.closed_at || "—") + "" + "" + fmtHold(t.hold_seconds) + "" + "" + (t.strategy_tag || "—") + "" + "" + (t.reviewed ? "已复盘" : "待复盘") + "" + "" + "" ); }) .join(""); tbody.querySelectorAll(".or-edit-btn").forEach(function (btn) { btn.addEventListener("click", function () { openEdit(Number(btn.getAttribute("data-id"))); }); }); }) .catch(function () { tbody.innerHTML = '加载失败'; }); } function renderGroup(title, items) { if (!items || !items.length) { return ( '
' + title + '
无数据
' ); } var lines = items .slice(0, 8) .map(function (g) { return ( "
" + "" + g.key + " · " + 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?" + qs(), { 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 ( '
' + pair[0] + '
' + pair[1] + "
" ); }) .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 uploadSlot(input, file) { if (!draftId || !file) return; var status = input.parentElement && input.parentElement.querySelector(".or-upload-status"); var hidden = input.parentElement && input.parentElement.querySelector(".or-upload-hidden"); if (status) status.textContent = "上传中…"; 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 = "已上传"; input.value = ""; }) .catch(function () { if (hidden) hidden.value = ""; if (status) status.textContent = "失败"; }); } function openEdit(tradeId) { currentTradeId = tradeId; draftId = newDraftId(); var modal = $("or-edit-modal"); var body = $("or-edit-body"); var title = $("or-edit-title"); if (!modal || !body) return; body.innerHTML = '
加载中…
'; modal.style.display = "flex"; fetch("/api/options/review/trades/" + tradeId, { credentials: "same-origin" }) .then(function (r) { return r.json(); }) .then(function (data) { if (!data.ok || !data.trade) { body.innerHTML = '
加载失败
'; return; } var t = data.trade; var e = t.entry || {}; if (title) title.textContent = (t.source_label || "") + " · 复盘 #" + t.id; var legsHtml = ""; if (t.legs && t.legs.length) { legsHtml = '' + t.legs .map(function (leg) { return ( "" ); }) .join("") + "
合约盈亏原因
" + (leg.leg_role || "") + "" + (leg.inst_id || leg.symbol || "") + "" + fmtPnl(leg.realized_pnl) + "" + (leg.close_reason || "") + "
"; } var slots = SLOT_TFS.map(function (tf) { return ( '
' + "" + '' + '
' ); }).join(""); body.innerHTML = '
' + (t.inst_id || t.underlying || "") + " · 盈亏 " + fmtPnl(t.realized_pnl_total) + (t.is_hedge ? " (永续 " + fmtPnl(t.realized_pnl_perp) + " / 期权 " + fmtPnl(t.realized_pnl_options) + ")" : "") + "
开 " + (t.opened_at || "—") + " → 平 " + (t.closed_at || "—") + "
" + legsHtml + '
' + '' + '' + '' + '" + '' + '' + "
" + '" + '" + '
截图' + slots + "
" + '
' + '' + '' + "
"; body.querySelectorAll(".or-upload-input").forEach(function (input) { input.addEventListener("change", function () { var file = input.files && input.files[0]; if (file) uploadSlot(input, file); }); }); // 回填已有图片文件名到 hidden(仅展示状态) (e.images || []).forEach(function (img) { var hidden = body.querySelector('.or-upload-hidden[data-tf="' + img.tf + '"]'); var status = hidden && hidden.parentElement.querySelector(".or-upload-status"); if (hidden && img.file) { hidden.value = img.file; if (status) status.textContent = "已有 " + img.file; } }); $("or-save-btn").addEventListener("click", saveEntry); $("or-del-btn").addEventListener("click", deleteEntry); }); } function esc(s) { return String(s == null ? "" : s) .replace(/&/g, "&") .replace(/" + v + ""; } function collectImages() { var body = $("or-edit-body"); if (!body) return []; var out = []; body.querySelectorAll(".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 saveEntry() { if (!currentTradeId) return; var payload = { trade_id: currentTradeId, strategy_tag: ($("or-f-strategy") || {}).value || "", 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: ($("or-f-mistakes") || {}).value || "", entry_logic: ($("or-f-entry") || {}).value || "", note: ($("or-f-note") || {}).value || "", images: collectImages(), }; 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) { alert(data.msg || "保存失败"); return; } closeModal(); reloadAll(); }) .catch(function () { alert("保存失败"); }); } function deleteEntry() { if (!currentTradeId) return; if (!confirm("删除该条复盘内容与图片?")) return; fetch("/api/options/review/entry/" + currentTradeId, { method: "DELETE", credentials: "same-origin", }) .then(function (r) { return r.json(); }) .then(function () { closeModal(); reloadAll(); }); } function closeModal(ev) { if (ev && ev.target && ev.target.id !== "or-edit-modal") return; var modal = $("or-edit-modal"); if (modal) modal.style.display = "none"; currentTradeId = null; } function init() { if (!$("options-review-root")) return; var syncBtn = $("or-sync-btn"); var reloadBtn = $("or-reload-btn"); if (syncBtn) syncBtn.addEventListener("click", syncNow); if (reloadBtn) reloadBtn.addEventListener("click", reloadAll); ["or-filter-source", "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); } ); reloadAll(); } global.OptionsReview = { init: init, closeModal: closeModal, openEdit: openEdit }; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } })(typeof window !== "undefined" ? window : globalThis);