Files
crypto_okx/lib/common/static/options_data_stats.js
T

343 lines
9.7 KiB
JavaScript

/**
* 实例数据统计:复盘 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, "&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) + "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 (
'<div class="ods-stat-row">' +
'<span class="ods-stat-key">' +
escapeHtml(keyLabel) +
" · " +
g.count +
"笔</span>" +
'<span class="ods-stat-val">' +
fmtPnl(g.pnl_sum) +
" / 胜" +
(g.win_rate || 0) +
"%</span>" +
"</div>"
);
})
.join("");
return (
'<div class="ods-stat-card"><div class="ods-stat-card-title">' +
title +
"</div>" +
lines +
"</div>"
);
}
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 = '<div class="muted" style="font-size:.76rem">统计加载失败</div>';
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 (
'<div class="ods-kpi-tile"><div class="ods-kpi-label">' +
pair[0] +
'</div><div class="ods-kpi-value' +
(cls ? " " + cls : "") +
'">' +
pair[1] +
"</div></div>"
);
})
.join("");
groups.innerHTML =
'<div class="muted" style="font-size:.76rem">数据统计口径:期权历史盈亏×平仓卖回价(与复盘账户差分独立)。单位 ' +
unit +
(avg != null ? " · 笔均 " + fmtPnl(avg) : "") +
"</div>";
})
.catch(function () {
kpi.innerHTML = '<div class="muted" style="font-size:.76rem">统计加载失败</div>';
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 =
'<text x="320" y="120" text-anchor="middle" fill="' +
muted +
'" font-size="13">暂无已实现盈亏数据</text>';
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 (
'<line x1="' +
padL +
'" y1="' +
y.toFixed(1) +
'" x2="' +
(W - padR) +
'" y2="' +
y.toFixed(1) +
'" stroke="' +
grid +
'" stroke-width="1"/>' +
'<text x="' +
(padL - 6) +
'" y="' +
(y + 4).toFixed(1) +
'" text-anchor="end" fill="' +
muted +
'" font-size="10">' +
Number(v).toFixed(0) +
"</text>"
);
})
.join("");
var last = points[points.length - 1];
svg.innerHTML =
gridHtml +
'<path d="' +
path +
'" fill="none" stroke="' +
stroke +
'" stroke-width="2" vector-effect="non-scaling-stroke"/>' +
'<circle cx="' +
xOf(last.t).toFixed(1) +
'" cy="' +
yOf(last.v).toFixed(1) +
'" r="3.5" fill="' +
stroke +
'"/>';
}
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);