ed3033d793
Co-authored-by: Cursor <cursoragent@cursor.com>
297 lines
9.5 KiB
JavaScript
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, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """);
|
|
}
|
|
|
|
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 */
|
|
},
|
|
};
|
|
})();
|