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