Files
crypto_monitor/manual_trading_hub/static/compare.js
T
2026-07-23 14:39:59 +08:00

297 lines
9.5 KiB
JavaScript

/**
* 中控策略对比:同风险额 R 下 合约 / 单期权 / 期期7:3
*/
(function () {
const page = document.getElementById("page-compare");
if (!page) return;
let inited = false;
let calcTimer = null;
function $(id) {
return document.getElementById(id);
}
function esc(s) {
return String(s == null ? "" : s)
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function num(id) {
const el = $(id);
if (!el) return null;
const n = Number(el.value);
return Number.isFinite(n) ? n : null;
}
function text(id) {
const el = $(id);
return el ? String(el.value || "").trim() : "";
}
function fmtU(v) {
if (v == null || !Number.isFinite(Number(v))) return "—";
const n = Number(v);
const abs = Math.abs(n).toFixed(2);
if (Math.abs(n) < 1e-9) return "0.00U";
return (n > 0 ? "+" : "-") + abs + "U";
}
function pnlClass(v) {
const n = Number(v);
if (!Number.isFinite(n) || Math.abs(n) < 1e-9) return "";
return n > 0 ? "cmp-pnl-pos" : "cmp-pnl-neg";
}
function setStatus(msg, isErr) {
const el = $("cmp-status");
if (!el) return;
el.textContent = msg || "";
el.className = "toolbar-meta" + (isErr ? " err" : "");
}
function syncDirectionDefaults() {
const dir = text("cmp-direction") || "long";
const isLong = dir === "long";
const optType = $("cmp-opt-type");
const mainType = $("cmp-hedge-main-type");
const sideType = $("cmp-hedge-side-type");
if (optType && !optType.dataset.touched) optType.value = isLong ? "C" : "P";
if (mainType && !mainType.dataset.touched) mainType.value = isLong ? "C" : "P";
if (sideType && !sideType.dataset.touched) sideType.value = isLong ? "P" : "C";
}
function collectPayload() {
const tp = num("cmp-tp");
return {
base: text("cmp-base") || "ETH",
direction: text("cmp-direction") || "long",
entry: num("cmp-entry"),
sl: num("cmp-sl"),
tp: tp,
risk_u: num("cmp-risk"),
tp_opt: num("cmp-tp-opt") != null ? num("cmp-tp-opt") : tp,
tp_hedge: num("cmp-tp-hedge") != null ? num("cmp-tp-hedge") : tp,
option: {
opt_type: text("cmp-opt-type") || "C",
strike: num("cmp-opt-strike"),
ask: num("cmp-opt-ask"),
},
hedge: {
main: {
opt_type: text("cmp-hedge-main-type") || "C",
strike: num("cmp-hedge-main-strike"),
ask: num("cmp-hedge-main-ask"),
},
side: {
opt_type: text("cmp-hedge-side-type") || "P",
strike: num("cmp-hedge-side-strike"),
ask: num("cmp-hedge-side-ask"),
},
},
};
}
function renderSummaryCards(data) {
const box = $("cmp-summary");
if (!box) return;
const perp = data.perp || {};
const opt = data.option || {};
const hedge = data.hedge || {};
const cards = [];
cards.push(`<article class="cmp-sum-card card">
<h3>单独合约</h3>
<div class="cmp-sum-row"><span>张数</span><strong>${esc(perp.sheets)}</strong></div>
<div class="cmp-sum-row"><span>止损占用</span><strong>${fmtU(perp.risk_used_u)}</strong></div>
<div class="cmp-sum-row"><span>面值</span><strong>${esc(perp.contract_size)} 币/张</strong></div>
</article>`);
if (opt.ok) {
cards.push(`<article class="cmp-sum-card card">
<h3>单独期权 · ${esc(opt.opt_type)} ${esc(opt.strike)}</h3>
<div class="cmp-sum-row"><span>张数</span><strong>${esc(opt.sheets)}</strong></div>
<div class="cmp-sum-row"><span>权利金</span><strong>${fmtU(opt.premium_u)}</strong></div>
<div class="cmp-sum-row"><span>单张成本</span><strong>${fmtU(opt.unit_cost_u)}</strong></div>
</article>`);
} else {
cards.push(`<article class="cmp-sum-card card">
<h3>单独期权</h3>
<p class="cmp-muted">${esc(opt.msg || "输入不完整")}</p>
</article>`);
}
if (hedge.ok) {
const m = hedge.main || {};
const s = hedge.side || {};
cards.push(`<article class="cmp-sum-card card">
<h3>期期对冲 7:3</h3>
<div class="cmp-sum-row"><span>主腿 ${esc(m.opt_type)} ${esc(m.strike)}</span><strong>${esc(m.sheets)} 张 · ${fmtU(m.premium_u)}</strong></div>
<div class="cmp-sum-row"><span>次腿 ${esc(s.opt_type)} ${esc(s.strike)}</span><strong>${esc(s.sheets)} 张 · ${fmtU(s.premium_u)}</strong></div>
<div class="cmp-sum-row"><span>总权利金</span><strong>${fmtU(hedge.premium_u)}</strong></div>
</article>`);
} else {
cards.push(`<article class="cmp-sum-card card">
<h3>期期对冲</h3>
<p class="cmp-muted">${esc(hedge.msg || "输入不完整")}</p>
</article>`);
}
box.innerHTML = cards.join("");
}
function cell(v, note) {
const main = `<span class="${pnlClass(v)}">${fmtU(v)}</span>`;
if (!note) return main;
return `${main}<div class="cmp-cell-note">${esc(note)}</div>`;
}
function renderTable(data) {
const box = $("cmp-table-wrap");
if (!box) return;
const perp = data.perp || {};
const opt = data.option && data.option.ok ? data.option : null;
const hedge = data.hedge && data.hedge.ok ? data.hedge : null;
const dash = "—";
box.innerHTML = `<div class="cmp-table-scroll"><table class="cmp-table">
<thead>
<tr>
<th>路径</th>
<th>单独合约</th>
<th>单独期权</th>
<th>期期对冲</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>A 干净止盈</strong><div class="cmp-cell-note">盈利能力主对比</div></td>
<td>${cell(perp.path_a_tp)}</td>
<td>${opt ? cell(opt.path_a_tp) : dash}</td>
<td>${hedge ? cell(hedge.path_a_tp) : dash}</td>
</tr>
<tr>
<td><strong>B 打止损</strong><div class="cmp-cell-note">合约实现亏损;期权另注最坏</div></td>
<td>${cell(perp.path_b_sl)}</td>
<td>${
opt
? cell(opt.path_b_sl, "最坏到期亏满权利金 " + fmtU(opt.path_b_worst))
: dash
}</td>
<td>${
hedge
? cell(hedge.path_b_sl, "最坏双腿归零 " + fmtU(hedge.path_b_worst))
: dash
}</td>
</tr>
<tr>
<td><strong>C 先止损再去止盈</strong><div class="cmp-cell-note">合约踏空对照</div></td>
<td>${cell(
perp.path_c_realized,
"踏空未拿到 " + fmtU(perp.path_c_missed)
)}</td>
<td>${opt ? cell(opt.path_c_hold_to_tp, opt.path_c_note || "") : dash}</td>
<td>${hedge ? cell(hedge.path_c_hold_to_tp, hedge.path_c_note || "") : dash}</td>
</tr>
</tbody>
</table></div>`;
}
function renderRecommend(data) {
const box = $("cmp-recommend");
if (!box) return;
const rec = data.recommend || {};
const bullets = Array.isArray(rec.bullets) ? rec.bullets : [];
const warns = Array.isArray(data.warnings) ? data.warnings : [];
box.innerHTML = `<div class="cmp-rec-card card">
<div class="cmp-rec-head">推荐:<strong>${esc(rec.choice || "—")}</strong></div>
<p class="cmp-rec-reason">${esc(rec.reason || "")}</p>
<ul class="cmp-rec-list">${bullets.map((b) => `<li>${esc(b)}</li>`).join("")}</ul>
${
warns.length
? `<div class="cmp-warn">${warns.map((w) => esc(w)).join(" · ")}</div>`
: ""
}
<p class="cmp-foot-note">${(data.notes || []).map(esc).join(" · ")}</p>
</div>`;
}
async function runCalc() {
const payload = collectPayload();
if (
payload.entry == null ||
payload.sl == null ||
payload.tp == null ||
payload.risk_u == null
) {
setStatus("请填写入场 / 止损 / 止盈 / 风险额", true);
return;
}
setStatus("计算中…");
try {
const r = await fetch("/api/compare/calc", {
method: "POST",
credentials: "same-origin",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const data = await r.json();
if (!data.ok) {
setStatus(data.msg || "计算失败", true);
return;
}
renderSummaryCards(data);
renderTable(data);
renderRecommend(data);
setStatus("已更新");
} catch (e) {
setStatus(String(e.message || e), true);
}
}
function scheduleCalc() {
if (calcTimer) clearTimeout(calcTimer);
calcTimer = setTimeout(() => {
void runCalc();
}, 280);
}
function bind() {
const form = $("cmp-form");
if (!form || form.dataset.bound === "1") return;
form.dataset.bound = "1";
form.addEventListener("submit", (ev) => {
ev.preventDefault();
void runCalc();
});
form.querySelectorAll("input, select").forEach((el) => {
el.addEventListener("change", () => {
if (el.id === "cmp-direction") syncDirectionDefaults();
if (
el.id === "cmp-opt-type" ||
el.id === "cmp-hedge-main-type" ||
el.id === "cmp-hedge-side-type"
) {
el.dataset.touched = "1";
}
scheduleCalc();
});
el.addEventListener("input", scheduleCalc);
});
const btn = $("cmp-btn-run");
if (btn) btn.addEventListener("click", () => void runCalc());
}
window.hubComparePage = {
init() {
if (!inited) {
bind();
syncDirectionDefaults();
inited = true;
}
scheduleCalc();
},
destroy() {
/* keep form state */
},
};
})();