5c3969674a
单独期权与中控改为盈亏比×权利金触发买一平仓,默认2;不达标等到期。 Co-authored-by: Cursor <cursoragent@cursor.com>
281 lines
12 KiB
JavaScript
281 lines
12 KiB
JavaScript
(function (global) {
|
||
"use strict";
|
||
|
||
function fmt(v, d) {
|
||
if (v === null || v === undefined || Number.isNaN(Number(v))) return "—";
|
||
return Number(v).toFixed(d == null ? 2 : d);
|
||
}
|
||
|
||
function fmtDisplay(v, fallback) {
|
||
if (v !== null && v !== undefined && String(v).trim() !== "") return String(v);
|
||
if (fallback !== undefined) return fmtDisplay(fallback);
|
||
return "—";
|
||
}
|
||
|
||
function fmtOptionPx(v, tickSz) {
|
||
if (v === null || v === undefined || Number.isNaN(Number(v))) return "—";
|
||
const n = Number(v);
|
||
const tick = Number(tickSz);
|
||
if (!tickSz || Number.isNaN(tick) || tick <= 0) {
|
||
let s = n.toFixed(4).replace(/\.?0+$/, "");
|
||
return s || "0";
|
||
}
|
||
let decimals = 0;
|
||
if (tick < 1) decimals = Math.max(0, -Math.round(Math.log10(tick)));
|
||
else if (String(tick).indexOf(".") >= 0) decimals = String(tick).split(".")[1].length;
|
||
let s = n.toFixed(decimals);
|
||
// 仅裁小数尾零;整数 tick(BTC=5)时绝不能把 1370 裁成 137
|
||
if (decimals > 0) s = s.replace(/\.?0+$/, "");
|
||
return s || "0";
|
||
}
|
||
|
||
function fmtUsdc(v) {
|
||
if (v === null || v === undefined || Number.isNaN(Number(v))) return "—";
|
||
return Number(v).toFixed(2);
|
||
}
|
||
|
||
function optTypeLabel(t) {
|
||
return (t || "").toUpperCase() === "P" ? "看跌 Put" : "看涨 Call";
|
||
}
|
||
|
||
function sourceText(p) {
|
||
const lab = (p && p.source_label) || "纯期权";
|
||
const src = (p && p.source) || "option";
|
||
let pid = p && p.source_plan_id;
|
||
if (pid == null && p && p.hedge_plan_target && p.hedge_plan_target.plan_id != null) {
|
||
pid = p.hedge_plan_target.plan_id;
|
||
}
|
||
if (src !== "option" && pid != null && pid !== "") return lab + " #" + pid;
|
||
return lab;
|
||
}
|
||
|
||
function sourceBadgeHtml(p) {
|
||
const src = (p && p.source) || "option";
|
||
const cls =
|
||
src === "options_options"
|
||
? "opt-source-badge opt-source-badge--oo"
|
||
: src === "perp_options"
|
||
? "opt-source-badge opt-source-badge--po"
|
||
: "opt-source-badge opt-source-badge--plain";
|
||
return '<span class="' + cls + '" title="持仓来源">' + sourceText(p) + "</span>";
|
||
}
|
||
|
||
function pnlCls(upl, hub) {
|
||
if (upl > 0) return hub ? "pnl-pos" : "pos-pnl-profit";
|
||
if (upl < 0) return hub ? "pnl-neg" : "pos-pnl-loss";
|
||
return "";
|
||
}
|
||
|
||
function fmtPxSz(px, sz, tickSz) {
|
||
if (px === null || px === undefined || Number.isNaN(Number(px))) return "—";
|
||
let price = fmtOptionPx(px, tickSz);
|
||
if (sz === null || sz === undefined || sz === "" || Number.isNaN(Number(sz))) return price;
|
||
const s = Number(sz);
|
||
const size = Math.abs(s - Math.round(s)) < 1e-9 ? String(Math.round(s)) : String(s);
|
||
return price + "/" + size;
|
||
}
|
||
|
||
/** 买盘深度:价格/流动性;仅展示平仓所需档位(买一不够才出买二…). */
|
||
function fmtCloseLevels(preview, tickSz) {
|
||
if (preview && preview.bid_invalid) {
|
||
return "暂无有效买盘";
|
||
}
|
||
const levels = ((preview && preview.levels) || []).slice(0, 5);
|
||
if (!levels.length) return "—";
|
||
return levels.map(function (x, idx) {
|
||
const levelNo = x.level != null ? x.level : idx + 1;
|
||
const liq = x.available_sheets != null ? x.available_sheets : x.sz;
|
||
return "买" + levelNo + " " + fmtPxSz(x.px, liq, tickSz);
|
||
}).join(" · ");
|
||
}
|
||
|
||
function closeGateHint(preview) {
|
||
if (!preview) return "";
|
||
if (preview.bid_invalid || preview.manual_close_blocked) {
|
||
return preview.bid_invalid_reason || "当前买一无效,禁止买一平仓";
|
||
}
|
||
const gate = preview.close_gate || {};
|
||
if (preview.close_gate_blocked || (gate.ready === false && !gate.passed)) {
|
||
return "目标门控: " + (preview.close_gate_msg || gate.msg || "可回收需≥2×权利金并持续2分钟");
|
||
}
|
||
return "";
|
||
}
|
||
|
||
function netPnlFromPos(p) {
|
||
const preview = (p && p.close_preview) || {};
|
||
if (preview.bid_invalid) {
|
||
const upl = p && p.upl != null ? Number(p.upl) : NaN;
|
||
return Number.isFinite(upl) ? upl : null;
|
||
}
|
||
if (preview.estimated_pnl != null && !Number.isNaN(Number(preview.estimated_pnl))) {
|
||
return Number(preview.estimated_pnl);
|
||
}
|
||
const covered = Number(preview.covered_sheets);
|
||
const recv = Number(preview.total_received);
|
||
const prem = Number(p && p.premium_paid);
|
||
if (
|
||
preview.total_received != null &&
|
||
Number.isFinite(covered) &&
|
||
covered > 0 &&
|
||
!Number.isNaN(recv) &&
|
||
!Number.isNaN(prem)
|
||
) {
|
||
return recv - prem;
|
||
}
|
||
const upl = p && p.upl != null ? Number(p.upl) : NaN;
|
||
return Number.isFinite(upl) ? upl : null;
|
||
}
|
||
|
||
function netRoiFromPos(p, net) {
|
||
const preview = (p && p.close_preview) || {};
|
||
if (preview.estimated_pnl_ratio_pct != null && !Number.isNaN(Number(preview.estimated_pnl_ratio_pct))) {
|
||
return Number(preview.estimated_pnl_ratio_pct);
|
||
}
|
||
const prem = Number(p && p.premium_paid);
|
||
if (net == null || Number.isNaN(prem) || prem <= 0) return null;
|
||
return (net / prem) * 100;
|
||
}
|
||
|
||
function fmtClosePreview(preview, premiumPaid, hub) {
|
||
if (!preview || preview.total_received == null) return "—";
|
||
const recvTxt = fmtUsdc(preview.total_received);
|
||
let cls = "";
|
||
const prem = Number(premiumPaid);
|
||
const recv = Number(preview.total_received);
|
||
if (!Number.isNaN(prem) && !Number.isNaN(recv)) {
|
||
if (recv > prem) cls = " " + pnlCls(1, hub);
|
||
else if (recv < prem) cls = " " + pnlCls(-1, hub);
|
||
}
|
||
return '<span class="opt-close-value' + cls + '">' + recvTxt + " USDC</span>";
|
||
}
|
||
|
||
function expiryCdHtml(expMs) {
|
||
const ms = expMs != null && expMs !== "" ? String(expMs) : "";
|
||
if (!ms) return "—";
|
||
return '<span class="opt-expiry-cd" data-opt-exp-ms="' + ms + '">—</span>';
|
||
}
|
||
|
||
function renderCardInner(p, opts) {
|
||
opts = opts || {};
|
||
const hub = !!opts.hub;
|
||
const readOnly = !!opts.readOnly;
|
||
const hidePnl = !!opts.hidePnl;
|
||
const net = hidePnl ? null : netPnlFromPos(p);
|
||
const roi = hidePnl ? null : netRoiFromPos(p, net);
|
||
const uplCls = hidePnl ? "" : pnlCls(net, hub);
|
||
const sideCls = (p.opt_type || "").toUpperCase() === "P" ? "pos-side-short" : "pos-side-long";
|
||
const expMs = p.exp_time_ms != null ? p.exp_time_ms : p.exp_time;
|
||
const expAttr = expMs != null && expMs !== "" ? String(expMs) : "";
|
||
const closePreview = p.close_preview || {};
|
||
const tickSz = p.tick_sz;
|
||
const premTxt = fmtDisplay(p.premium_paid_fmt, p.premium_paid != null ? fmtUsdc(p.premium_paid) : null);
|
||
const avgTxt = p.avg_px != null ? fmtOptionPx(p.avg_px, tickSz) : fmtDisplay(p.avg_px_fmt);
|
||
const markTxt = p.mark_px != null ? fmtOptionPx(p.mark_px, tickSz) : fmtDisplay(p.mark_px_fmt);
|
||
let headActions = "";
|
||
if (!readOnly) {
|
||
const closeSheets = p.avail_pos != null && Number(p.avail_pos) > 0 ? p.avail_pos : p.pos;
|
||
headActions =
|
||
'<div class="pos-head-actions">' +
|
||
'<button type="button" class="btn-primary opt-close-btn" data-inst="' + (p.inst_id || "") + '" data-sheets="' + closeSheets + '">买一平仓</button>' +
|
||
"</div>";
|
||
}
|
||
const pnlCells = hidePnl
|
||
? ""
|
||
: '<div class="pos-cell"><span class="pos-label">净盈亏</span><span class="pos-value ' + uplCls + '">' +
|
||
(net == null ? "—" : fmt(net, 2)) + "</span></div>" +
|
||
'<div class="pos-cell"><span class="pos-label">收益率</span><span class="pos-value ' + uplCls + '">' +
|
||
(roi == null ? "—" : fmt(roi, 2) + "%") + "</span></div>";
|
||
return (
|
||
'<div class="pos-card-head">' +
|
||
'<div class="pos-card-symbol"><strong>' + (p.inst_id || "") + "</strong>" +
|
||
'<span class="pos-side-badge ' + sideCls + '">' + optTypeLabel(p.opt_type) + "</span>" +
|
||
sourceBadgeHtml(p) +
|
||
"</div>" +
|
||
headActions +
|
||
"</div>" +
|
||
'<div class="pos-meta">' +
|
||
'<span class="pos-meta-item">持仓来源: ' + sourceText(p) + "</span>" +
|
||
'<span class="pos-meta-item">行权价: ' + fmt(p.strike, 0) + "</span>" +
|
||
'<span class="pos-meta-item">张数: ' + fmt(p.pos, 0) + " · 币量 " + fmt(p.eth_amount, 4) + "</span>" +
|
||
(expAttr
|
||
? '<span class="pos-meta-item">到期倒计时: ' + expiryCdHtml(expAttr) + "</span>"
|
||
: "") +
|
||
"</div>" +
|
||
'<div class="pos-grid">' +
|
||
'<div class="pos-cell"><span class="pos-label">权利金</span><span class="pos-value">' + premTxt + " USDC</span></div>" +
|
||
'<div class="pos-cell"><span class="pos-label">开仓均价</span><span class="pos-value">' + avgTxt + "</span></div>" +
|
||
'<div class="pos-cell"><span class="pos-label">标记价</span><span class="pos-value">' + markTxt + "</span></div>" +
|
||
'<div class="pos-cell"><span class="pos-label">指数价</span><span class="pos-value">' + fmt(p.idx_px, 0) + "</span></div>" +
|
||
'<div class="pos-cell"><span class="pos-label">到期平衡</span><span class="pos-value">' + fmt(p.expiry_be_px, 0) + "</span></div>" +
|
||
'<div class="pos-cell"><span class="pos-label">平掉回本</span><span class="pos-value">' + fmt(p.close_be_px, 0) + "</span></div>" +
|
||
pnlCells +
|
||
'<div class="pos-cell opt-pos-cell--depth"><span class="pos-label">买盘深度</span><span class="pos-value opt-bid-plain">' + fmtCloseLevels(closePreview, tickSz) + "</span></div>" +
|
||
'<div class="pos-cell opt-pos-cell--close"><span class="pos-label">按买盘回收</span><span class="pos-value">' +
|
||
(closePreview.bid_invalid
|
||
? '<span class="muted">暂无有效买盘</span>'
|
||
: fmtClosePreview(closePreview, hidePnl ? null : p.premium_paid, hub)) + "</span></div>" +
|
||
"</div>" +
|
||
(function () {
|
||
const hint = closeGateHint(closePreview);
|
||
return hint ? '<div class="muted opt-bid-invalid-hint">' + hint + "</div>" : "";
|
||
})() +
|
||
(p.profit_rr != null || p.target_index != null
|
||
? (function () {
|
||
const hedgeTarget = p.hedge_plan_target || null;
|
||
const managed = hedgeTarget && hedgeTarget.managed_by === "hedge_plan";
|
||
const rr =
|
||
managed && hedgeTarget.oo_profit_rr != null
|
||
? Number(hedgeTarget.oo_profit_rr)
|
||
: p.profit_rr != null
|
||
? Number(p.profit_rr)
|
||
: null;
|
||
const prem = Number(p.premium_paid);
|
||
let profit = null;
|
||
let need = null;
|
||
if (rr != null && Number.isFinite(rr) && rr > 0 && Number.isFinite(prem) && prem > 0) {
|
||
profit = Math.round(prem * rr * 100) / 100;
|
||
need = Math.round((prem + profit) * 100) / 100;
|
||
}
|
||
const profitTxt = profit == null ? "—" : ((profit > 0 ? "+" : "") + fmtUsdc(profit) + " USDC");
|
||
const profitCls = profit > 0 ? " pnl-pos" : profit < 0 ? " pnl-neg" : "";
|
||
const profitSpan = hidePnl
|
||
? ""
|
||
: '<span class="pos-value' + profitCls + '">目标盈利 ' + profitTxt + "</span>";
|
||
const ruleTxt =
|
||
rr != null && Number.isFinite(rr) && rr > 0
|
||
? "盈亏比 ×" + fmt(rr, 2)
|
||
: p.target_index != null
|
||
? "目标 " + fmt(p.target_index, 1)
|
||
: "委托中";
|
||
return (
|
||
'<div class="opt-target-row opt-target-row--ro' + (managed ? " opt-target-row--managed" : "") + '">' +
|
||
'<span class="opt-target-row-label">' + (managed ? "对冲计划 #" + hedgeTarget.plan_id : "委托") + "</span>" +
|
||
'<span class="pos-value">' + ruleTxt + "</span>" +
|
||
(need != null ? '<span class="pos-value">需回收 ' + fmtUsdc(need) + " USDC</span>" : "") +
|
||
profitSpan +
|
||
'<span class="muted opt-target-row-hint">' +
|
||
(managed ? "进行中 · 由对冲计划监控" : "监控中 · 买一浮盈达盈亏比后全平") +
|
||
"</span></div>"
|
||
);
|
||
})()
|
||
: "")
|
||
);
|
||
}
|
||
|
||
function renderCard(p, opts) {
|
||
opts = opts || {};
|
||
const hub = !!opts.hub;
|
||
const extraCls = hub ? " hub-pos-card hub-opt-pos-card" : " opt-pos-card";
|
||
return (
|
||
'<div class="pos-card' + extraCls + '" data-inst="' + (p.inst_id || "") + '">' +
|
||
renderCardInner(p, opts) +
|
||
"</div>"
|
||
);
|
||
}
|
||
|
||
global.OptionsPositionCards = {
|
||
renderCardInner: renderCardInner,
|
||
renderCard: renderCard,
|
||
};
|
||
})(typeof window !== "undefined" ? window : globalThis);
|