/** * 实例数据统计:复盘 KPI + 已实现盈亏曲线. */ (function (global) { "use strict"; var currentRange = "7d"; var booted = false; function $(id) { return document.getElementById(id); } function root() { return $("options-data-stats"); } 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 pnlClass(v) { var n = Number(v); if (Number.isNaN(n) || n === 0) return ""; return n > 0 ? "ods-pnl-pos" : "ods-pnl-neg"; } function closeReasonLabel(r) { var map = { perp_tp: "永续止盈", perp_sl: "永续止损", oo_expiry_loss: "期期到期亏损", oo_expiry_win: "期期到期盈利", target_win_leg: "期期平盈利腿", profit_rr_win_leg: "期期盈亏比达标·平盈利腿", }; return map[r] || String(r || "未填"); } function sourceType() { var el = root(); return (el && el.getAttribute("data-source-type")) || "option_spot"; } function setStatus(text) { var el = $("ods-status"); if (el) el.textContent = text || ""; } 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 loadReviewStats() { var kpi = $("ods-kpi"); var groups = $("ods-stats-groups"); if (!kpi || !groups) return Promise.resolve(); // 口径 B:期权历史币盈亏×卖回价(/api/options/stats),与复盘账户差分独立 return fetch("/api/options/stats", { credentials: "same-origin" }) .then(function (r) { return r.json(); }) .then(function (data) { if (!data || !data.ok) { kpi.innerHTML = '
统计加载失败
'; groups.innerHTML = ""; return; } var unit = data.pnl_unit || "U"; var net = data.net_realized_pnl; var avg = data.total_closed > 0 && net != null ? Number(net) / Number(data.total_closed) : null; kpi.innerHTML = [ ["已平笔数", data.total_closed || 0], ["胜率", (data.win_rate || 0) + "%"], ["盈亏比", data.profit_loss_ratio != null ? data.profit_loss_ratio : "—"], ["累计盈亏", fmtPnl(net)], ["平均盈利", data.avg_win != null ? fmtPnl(data.avg_win) : "—"], ["平均亏损", data.avg_loss != null ? fmtPnl(-Math.abs(Number(data.avg_loss))) : "—"], ["平均持有", fmtHold(data.avg_hold_sec)], ] .map(function (pair) { var cls = ""; if (pair[0] === "累计盈亏") cls = pnlClass(net); if (pair[0] === "平均盈利") cls = "pnl-pos"; if (pair[0] === "平均亏损") cls = "pnl-neg"; return ( '
' + pair[0] + '
' + pair[1] + "
" ); }) .join(""); groups.innerHTML = '
数据统计口径:期权历史盈亏×平仓卖回价(与复盘账户差分独立)。单位 ' + unit + (avg != null ? " · 笔均 " + fmtPnl(avg) : "") + "
"; }) .catch(function () { kpi.innerHTML = '
统计加载失败
'; groups.innerHTML = ""; }); } function fmtCash(v) { if (v == null || Number.isNaN(Number(v))) return "—"; return Number(v).toFixed(2) + "U"; } function drawCurve(points) { var svg = $("ods-curve-svg"); if (!svg) return; var W = 640; var H = 240; var padL = 48; var padR = 12; var padT = 16; var padB = 28; var stroke = getComputedStyle(document.body).getPropertyValue("--accent") || "#5aa0ff"; stroke = (stroke || "").trim() || "#5aa0ff"; var grid = "rgba(127,127,127,.22)"; var muted = "rgba(160,160,180,.75)"; if (!points || !points.length) { svg.innerHTML = '暂无已实现盈亏数据'; return; } var vals = points.map(function (p) { return Number(p.v); }); var ts = points.map(function (p) { return Number(p.t); }); var minV = Math.min.apply(null, vals); var maxV = Math.max.apply(null, vals); if (maxV - minV < 1e-6) { minV -= 1; maxV += 1; } var minT = ts[0]; var maxT = ts[ts.length - 1]; if (maxT <= minT) maxT = minT + 1; function xOf(t) { return padL + ((t - minT) / (maxT - minT)) * (W - padL - padR); } function yOf(v) { return padT + (1 - (v - minV) / (maxV - minV)) * (H - padT - padB); } var path = points .map(function (p, i) { var x = xOf(p.t); var y = yOf(p.v); return (i ? "L" : "M") + x.toFixed(1) + "," + y.toFixed(1); }) .join(" "); var yTicks = [minV, (minV + maxV) / 2, maxV]; var gridHtml = yTicks .map(function (v) { var y = yOf(v); return ( '' + '' + Number(v).toFixed(0) + "" ); }) .join(""); var last = points[points.length - 1]; svg.innerHTML = gridHtml + '' + ''; } function loadCashCurve() { var meta = $("ods-curve-meta"); return fetch("/api/instance/cash_curve?range=" + encodeURIComponent(currentRange), { credentials: "same-origin", }) .then(function (r) { return r.json(); }) .then(function (data) { if (!data || !data.ok) { if (meta) meta.textContent = "曲线加载失败"; drawCurve([]); return; } var pts = data.points || []; drawCurve(pts); if (meta) { meta.textContent = "已实现 " + (data.trade_count != null ? data.trade_count + " 笔 · " : "") + pts.length + " 点 · 累计 " + fmtCash(data.latest != null ? data.latest : pts.length ? pts[pts.length - 1].v : null); } }) .catch(function () { if (meta) meta.textContent = "曲线加载失败"; drawCurve([]); }); } function reloadAll() { setStatus("加载中…"); return Promise.all([loadReviewStats(), loadCashCurve()]).then(function () { setStatus(""); }); } function bindOnce() { if (booted || !root()) return; booted = true; var btn = $("ods-reload-btn"); if (btn) { btn.addEventListener("click", function () { reloadAll(); }); } document.querySelectorAll("#options-data-stats .ods-range-btn").forEach(function (b) { b.addEventListener("click", function () { currentRange = b.getAttribute("data-range") || "7d"; document.querySelectorAll("#options-data-stats .ods-range-btn").forEach(function (x) { x.classList.toggle("active", x === b); }); loadCashCurve(); }); }); // 默认 7 日更有曲线感;若用户点今日再切 var def7 = document.querySelector('#options-data-stats .ods-range-btn[data-range="7d"]'); if (def7) { document.querySelectorAll("#options-data-stats .ods-range-btn").forEach(function (x) { x.classList.toggle("active", x === def7); }); currentRange = "7d"; } reloadAll(); } function bootIfPresent() { if (root()) bindOnce(); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", bootIfPresent); } else { bootIfPresent(); } // embed 软切 tab 时脚本可能再次插入 global.__OPTIONS_DATA_STATS_BOOT__ = bootIfPresent; })(typeof window !== "undefined" ? window : this);