Add hub strategy compare page for perp vs options vs 7:3 hedge.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,296 @@
|
||||
/**
|
||||
* 中控策略对比:同风险额 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 */
|
||||
},
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user