Files
crypto_monitor/lib/common/static/hedge_plan.js
T

510 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* OKX 对冲计划 P0:行情 + 永期列表 / 期期 T + 情景测算 + 门禁.
*/
(function () {
const root = document.getElementById("hedge-plan-root");
if (!root) return;
const state = {
mode: "perp_options",
underlying: root.getAttribute("data-default-underly") || "ETH",
chain: null,
selected: null,
legA: null,
legB: null,
market: null,
};
function $(id) {
return document.getElementById(id);
}
async function apiJson(url, opts) {
const res = await fetch(url, Object.assign({ credentials: "same-origin" }, opts || {}));
const data = await res.json().catch(function () {
return {};
});
if (!res.ok) throw new Error(data.msg || res.statusText || "请求失败");
return data;
}
function fmt(v, d) {
if (v === null || v === undefined || Number.isNaN(Number(v))) return "—";
return Number(v).toFixed(d == null ? 2 : d);
}
function optTypeForDirection(dir) {
return dir === "short" ? "C" : "P";
}
function syncModeUI() {
document.querySelectorAll(".hp-mode-btn").forEach(function (b) {
b.classList.toggle("active", b.getAttribute("data-mode") === state.mode);
});
const po = $("hp-po-layout");
const oo = $("hp-oo-layout");
if (po) po.classList.toggle("hidden", state.mode !== "perp_options");
if (oo) oo.classList.toggle("hidden", state.mode !== "options_options");
}
function setGateLine(gates) {
const el = $("hp-gate-line");
if (!el) return;
if (!gates) {
el.textContent = "";
return;
}
const parts = [
"计仓:" + (gates.is_full_margin ? "全仓" : "非全仓"),
"测算:" + (gates.can_preview ? "可" : "否"),
"开仓:" + (gates.can_start ? "可" : "否"),
];
if (gates.reasons && gates.reasons.length) parts.push(gates.reasons.join("; "));
el.textContent = parts.join(" · ");
const start = $("hp-start-btn");
if (start) start.disabled = !gates.can_start;
}
async function loadGates() {
try {
const d = await apiJson("/api/hedge-plan/gates?plan_type=" + encodeURIComponent(state.mode));
setGateLine(d);
} catch (e) {
setGateLine({ can_preview: false, can_start: false, reasons: [e.message], is_full_margin: false });
}
}
async function loadMarket() {
const dir = ($("hp-direction") && $("hp-direction").value) || "long";
const d = await apiJson(
"/api/hedge-plan/market?base=" +
encodeURIComponent(state.underlying) +
"&direction=" +
encodeURIComponent(dir)
);
state.market = d;
setGateLine(d.gates);
const q = $("hp-perp-quote");
if (q) {
q.innerHTML =
"标记 <strong>" +
fmt(d.mark, 2) +
"</strong> · 最新 " +
fmt(d.last, 2) +
" · 卖一 " +
fmt(d.ask, 2) +
" · 买一 " +
fmt(d.bid, 2) +
"<br/>面值 " +
fmt(d.contract_size, 4) +
" · 可用 " +
fmt(d.available_usdt, 2) +
" U";
}
const sz = $("hp-sizing-line");
if (sz) {
if (d.full_margin_sizing) {
const s = d.full_margin_sizing;
sz.textContent =
"全仓建议:保证金 " +
fmt(s.margin_capital, 2) +
"U × " +
s.leverage +
"x → 名义 " +
fmt(s.notional_value, 2) +
"U · 建议约 " +
fmt(d.suggest_contracts, 4) +
" 张";
} else {
sz.textContent = "非全仓或不具备保证金数据时仅手动填张数;永期开仓需全仓.";
}
}
const entry = $("hp-entry");
if (entry && d.entry_ref && !entry.value) entry.value = d.entry_ref;
const contracts = $("hp-contracts");
if (contracts && d.suggest_contracts != null && !contracts.value) {
contracts.value = d.suggest_contracts;
}
const label = $("hp-opt-type-label");
if (label) label.textContent = d.suggested_opt_type === "C" ? "Call" : "Put";
}
function fillExpSelect(sel, chain) {
if (!sel) return;
const prev = sel.value;
sel.innerHTML = '<option value="">选择到期日</option>';
(chain.expiries || []).forEach(function (e) {
const opt = document.createElement("option");
opt.value = String(e.exp_time);
const dt = new Date(Number(e.exp_time));
opt.textContent = dt.toLocaleString();
sel.appendChild(opt);
});
if (prev) sel.value = prev;
if (!sel.value && chain.expiries && chain.expiries[0]) {
sel.value = String(chain.expiries[0].exp_time);
}
}
async function loadChain() {
const d = await apiJson(
"/api/hedge-plan/options-chain?underlying=" + encodeURIComponent(state.underlying)
);
state.chain = d;
const idx = $("hp-index-line");
if (idx) idx.textContent = "指数 " + fmt(d.index_px, 2) + " · " + (d.inst_family || "");
const ooIdx = $("hp-oo-index");
if (ooIdx) ooIdx.textContent = "指数 " + fmt(d.index_px, 2);
fillExpSelect($("hp-exp-select"), d);
fillExpSelect($("hp-oo-exp-select"), d);
renderListStrikes();
renderTStrikes();
if (d.index_px && $("hp-target") && !$("hp-target").value) {
$("hp-target").value = d.index_px;
}
}
function currentExp(selectId) {
const sel = $(selectId);
const expMs = sel && sel.value;
if (!expMs || !state.chain) return null;
return (state.chain.expiries || []).find(function (e) {
return String(e.exp_time) === String(expMs);
});
}
function renderListStrikes() {
const tbody = $("hp-strike-tbody");
if (!tbody) return;
const dir = ($("hp-direction") && $("hp-direction").value) || "long";
const want = optTypeForDirection(dir);
const exp = currentExp("hp-exp-select");
tbody.innerHTML = "";
if (!exp) {
tbody.innerHTML = '<tr><td colspan="5" class="muted">请选择到期日</td></tr>';
return;
}
const list = (exp.contracts || []).filter(function (c) {
return String(c.opt_type || "").toUpperCase() === want;
});
if (!list.length) {
tbody.innerHTML = '<tr><td colspan="5" class="muted">无匹配合约</td></tr>';
return;
}
list.forEach(function (c) {
const tr = document.createElement("tr");
tr.innerHTML =
"<td>" +
c.strike +
"</td><td>" +
c.opt_type +
"</td><td>" +
fmt(c.ask, 4) +
"</td><td>" +
fmt(c.bid, 4) +
'</td><td><button type="button" class="btn-secondary hp-pick" data-inst="' +
c.inst_id +
'">选用</button></td>';
tbody.appendChild(tr);
});
tbody.querySelectorAll(".hp-pick").forEach(function (btn) {
btn.addEventListener("click", function () {
const inst = btn.getAttribute("data-inst");
const c = list.find(function (x) {
return x.inst_id === inst;
});
if (!c) return;
state.selected = c;
const el = $("hp-sel-inst");
if (el) el.textContent = c.inst_id;
updatePremiumLine();
});
});
}
function updatePremiumLine() {
const line = $("hp-premium-line");
if (!line || !state.selected) {
if (line) line.textContent = "";
return;
}
const sheets = Number(($("hp-sheets") && $("hp-sheets").value) || 1);
const ct = Number(state.selected.ct_mult || 0.01);
const ask = Number(state.selected.ask || 0);
const prem = ask * sheets * ct;
line.textContent = "预估权利金 ≈ " + fmt(prem, 4) + " USDC";
}
function buildStraddleRows(contracts) {
const map = {};
(contracts || []).forEach(function (c) {
const key = String(c.strike);
if (!map[key]) map[key] = { strike: c.strike, call: null, put: null };
const o = (c.opt_type || "").toUpperCase();
if (o === "C") map[key].call = c;
else if (o === "P") map[key].put = c;
});
return Object.keys(map)
.map(function (k) {
return map[k];
})
.sort(function (a, b) {
return Number(a.strike) - Number(b.strike);
});
}
function renderTStrikes() {
const tbody = $("hp-oo-tbody");
if (!tbody) return;
const exp = currentExp("hp-oo-exp-select");
tbody.innerHTML = "";
if (!exp) {
tbody.innerHTML = '<tr><td colspan="5" class="muted">请选择到期日</td></tr>';
return;
}
const rows = buildStraddleRows(exp.contracts);
rows.forEach(function (row) {
const tr = document.createElement("tr");
const callAsk = row.call ? fmt(row.call.ask, 4) : "—";
const putAsk = row.put ? fmt(row.put.ask, 4) : "—";
tr.innerHTML =
"<td>" +
callAsk +
'</td><td>' +
(row.call
? '<button type="button" class="btn-secondary hp-oo-pick" data-side="C" data-inst="' +
row.call.inst_id +
'">Call</button>'
: "—") +
'</td><td class="opt-t-strike"><strong>' +
row.strike +
"</strong></td><td>" +
putAsk +
'</td><td>' +
(row.put
? '<button type="button" class="btn-secondary hp-oo-pick" data-side="P" data-inst="' +
row.put.inst_id +
'">Put</button>'
: "—") +
"</td>";
tbody.appendChild(tr);
});
tbody.querySelectorAll(".hp-oo-pick").forEach(function (btn) {
btn.addEventListener("click", function () {
const inst = btn.getAttribute("data-inst");
const exp2 = currentExp("hp-oo-exp-select");
const c = (exp2.contracts || []).find(function (x) {
return x.inst_id === inst;
});
if (!c) return;
if (!state.legA) state.legA = c;
else if (!state.legB || state.legB.inst_id === state.legA.inst_id) state.legB = c;
else {
state.legA = c;
state.legB = null;
}
renderOoLegs();
});
});
}
function renderOoLegs() {
const el = $("hp-oo-legs");
if (!el) return;
function one(tag, c) {
if (!c) return tag + ": —";
return (
tag +
": " +
c.opt_type +
" K" +
c.strike +
" ask=" +
fmt(c.ask, 4) +
" (" +
c.inst_id +
")"
);
}
el.innerHTML = one("腿A", state.legA) + "<br/>" + one("腿B", state.legB);
}
function legPayload(c, sheets) {
return {
opt_type: c.opt_type,
strike: c.strike,
sheets: sheets,
ct_mult: c.ct_mult || 0.01,
ask: c.ask,
inst_id: c.inst_id,
};
}
async function runPreview() {
const tbody = $("hp-result-tbody");
const summary = $("hp-summary");
try {
let body;
if (state.mode === "options_options") {
if (!state.legA || !state.legB) throw new Error("请选用两条期权腿");
const target = Number(($("hp-target") && $("hp-target").value) || 0);
if (!target) throw new Error("请填写目标价");
body = {
plan_type: "options_options",
target_price: target,
index_px: (state.chain && state.chain.index_px) || target,
leg_a: legPayload(state.legA, 1),
leg_b: legPayload(state.legB, 1),
};
} else {
if (!state.selected) throw new Error("请选用期权腿");
const entry = Number(($("hp-entry") && $("hp-entry").value) || 0);
const tp = Number(($("hp-tp") && $("hp-tp").value) || 0);
const sl = Number(($("hp-sl") && $("hp-sl").value) || 0);
const contracts = Number(($("hp-contracts") && $("hp-contracts").value) || 0);
const sheets = Number(($("hp-sheets") && $("hp-sheets").value) || 1);
if (!entry || !tp || !sl || !contracts) throw new Error("请完整填写开仓/止盈/止损/张数");
body = {
plan_type: "perp_options",
direction: ($("hp-direction") && $("hp-direction").value) || "long",
entry: entry,
tp: tp,
sl: sl,
contracts: contracts,
contract_size: (state.market && state.market.contract_size) || 0.01,
opt_type: state.selected.opt_type,
strike: state.selected.strike,
sheets: sheets,
ct_mult: state.selected.ct_mult || 0.01,
ask: state.selected.ask,
index_px: state.chain && state.chain.index_px,
};
}
const d = await apiJson("/api/hedge-plan/preview", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
setGateLine(d.gates);
const s = d.summary || {};
if (summary) {
if (d.plan_type === "perp_options") {
summary.innerHTML =
"止盈合计 <strong>" +
fmt(s.tp_total) +
"</strong> · 止损合计 <strong>" +
fmt(s.sl_total) +
"</strong> · 保费 " +
fmt(s.premium_paid) +
(s.hedge_ratio_at_sl != null ? " · 止损对冲率 " + fmt(s.hedge_ratio_at_sl) + "%" : "");
} else {
summary.innerHTML =
"目标价合计 <strong>" +
fmt(s.at_target_total) +
"</strong> · 到期现价 <strong>" +
fmt(s.expiry_flat_total) +
"</strong> · 保费 " +
fmt(s.premium_paid) +
(s.expiry_is_loss ? " · 到期无盈利(记总亏损)" : "");
}
}
tbody.innerHTML = "";
(d.scenarios || []).forEach(function (sc) {
const tr = document.createElement("tr");
let mid;
if (sc.perp_pnl != null) {
mid = "永续 " + fmt(sc.perp_pnl);
} else {
mid = "A " + fmt(sc.leg_a_pnl) + " / B " + fmt(sc.leg_b_pnl);
}
const optCol = sc.options_pnl != null ? fmt(sc.options_pnl) : "—";
tr.innerHTML =
"<td>" +
(sc.label || sc.id) +
"</td><td>" +
fmt(sc.spot) +
"</td><td>" +
mid +
"</td><td>" +
optCol +
"</td><td><strong>" +
fmt(sc.total) +
"</strong></td><td class=\"muted\">" +
(sc.note || "") +
"</td>";
tbody.appendChild(tr);
});
} catch (e) {
if (tbody) tbody.innerHTML = '<tr><td colspan="6" class="err">' + (e.message || e) + "</td></tr>";
if (summary) summary.textContent = "";
}
}
function bind() {
document.querySelectorAll(".hp-mode-btn").forEach(function (b) {
b.addEventListener("click", function () {
state.mode = b.getAttribute("data-mode") || "perp_options";
syncModeUI();
loadGates();
});
});
document.querySelectorAll(".hp-uly-btn, .hp-uly-btn-oo").forEach(function (b) {
b.addEventListener("click", function () {
state.underlying = b.getAttribute("data-uly") || "ETH";
document.querySelectorAll(".hp-uly-btn, .hp-uly-btn-oo").forEach(function (x) {
x.classList.toggle("active", x.getAttribute("data-uly") === state.underlying);
});
state.selected = null;
state.legA = null;
state.legB = null;
void refreshAll();
});
});
const dir = $("hp-direction");
if (dir) {
dir.addEventListener("change", function () {
void loadMarket().then(function () {
renderListStrikes();
});
});
}
if ($("hp-refresh")) $("hp-refresh").addEventListener("click", function () {
void refreshAll();
});
if ($("hp-load-chain")) $("hp-load-chain").addEventListener("click", function () {
void loadChain();
});
if ($("hp-oo-load-chain")) $("hp-oo-load-chain").addEventListener("click", function () {
void loadChain();
});
if ($("hp-exp-select"))
$("hp-exp-select").addEventListener("change", renderListStrikes);
if ($("hp-oo-exp-select"))
$("hp-oo-exp-select").addEventListener("change", renderTStrikes);
if ($("hp-sheets")) $("hp-sheets").addEventListener("input", updatePremiumLine);
if ($("hp-preview-btn")) $("hp-preview-btn").addEventListener("click", function () {
void runPreview();
});
}
async function refreshAll() {
await loadGates();
try {
await loadMarket();
} catch (e) {
const q = $("hp-perp-quote");
if (q) q.textContent = e.message || String(e);
}
try {
await loadChain();
} catch (e) {
const tbody = $("hp-strike-tbody");
if (tbody) tbody.innerHTML = '<tr><td colspan="5" class="err">' + (e.message || e) + "</td></tr>";
}
}
syncModeUI();
bind();
void refreshAll();
})();