币本位持仓卡权利金/回收/门控改为按ETH/BTC展示,避免误标USDC与两位小数抹零

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-20 13:36:34 +08:00
parent 6c9825284f
commit 6c49c7d51c
8 changed files with 240 additions and 45 deletions
+94 -23
View File
@@ -868,9 +868,46 @@
return Number(v).toFixed(2);
}
function fmtClosePreview(preview, premiumPaid) {
function posPremiumCcy(p) {
const ccy = String((p && p.premium_ccy) || "").trim().toUpperCase();
if (ccy) return ccy;
const mode = String((p && p.margin_mode) || "").toLowerCase();
const inst = String((p && p.inst_id) || "");
if (mode === "coin" || (inst.indexOf("-USD-") >= 0 && inst.indexOf("_UM") < 0)) {
return (inst.split("-")[0] || "ETH").toUpperCase() || "ETH";
}
if (isCoinMarginMode && isCoinMarginMode()) {
return ((inst.split("-")[0]) || "ETH").toUpperCase() || "ETH";
}
return "USDC";
}
function isCoinPos(p) {
return posPremiumCcy(p) !== "USDC";
}
function fmtPremiumAmt(v, ccy) {
if (v === null || v === undefined || Number.isNaN(Number(v))) return "—";
const n = Number(v);
const unit = String(ccy || "USDC").toUpperCase();
if (unit === "ETH" || unit === "BTC") {
let s = n.toFixed(8).replace(/\.?0+$/, "");
return s || "0";
}
return fmtUsdc(n);
}
function fmtPremiumAmtSigned(v, ccy) {
if (v === null || v === undefined || Number.isNaN(Number(v))) return "—";
const n = Number(v);
const sign = n > 0 ? "+" : "";
return sign + fmtPremiumAmt(n, ccy) + " " + (String(ccy || "USDC").toUpperCase());
}
function fmtClosePreview(preview, premiumPaid, p) {
if (!preview || preview.total_received == null) return "—";
const recvTxt = fmtUsdc(preview.total_received);
const ccy = posPremiumCcy(p);
const recvTxt = fmtPremiumAmt(preview.total_received, ccy);
let cls = "";
const prem = Number(premiumPaid);
const recv = Number(preview.total_received);
@@ -878,12 +915,13 @@
if (recv > prem) cls = " pos-pnl-profit";
else if (recv < prem) cls = " pos-pnl-loss";
}
return '<span class="opt-close-value' + cls + '">' + recvTxt + " USDC</span>";
return '<span class="opt-close-value' + cls + '">' + recvTxt + " " + ccy + "</span>";
}
function fmtClosePreviewText(preview) {
function fmtClosePreviewText(preview, p) {
if (!preview || preview.total_received == null) return "—";
let text = fmt(preview.total_received, 4) + " USDC";
const ccy = posPremiumCcy(p);
let text = fmtPremiumAmt(preview.total_received, ccy) + " " + ccy;
if (preview.covered_sheets != null) {
text += " · 覆盖 " + preview.covered_sheets + "张";
}
@@ -893,11 +931,13 @@
return text;
}
function fmtPreviewLevels(preview) {
function fmtPreviewLevels(preview, p) {
const levels = (preview && preview.levels) || [];
if (!levels.length) return "暂无可用买盘深度";
const ccy = posPremiumCcy(p);
return levels.map(function (x) {
return "买" + x.level + " " + fmt(x.px, 4) + " × " + x.sheets + "张 ≈ " + fmt(x.received, 4) + " USDC";
return "买" + x.level + " " + fmt(x.px, 4) + " × " + x.sheets + "张 ≈ " +
fmtPremiumAmt(x.received, ccy) + " " + ccy;
}).join("\n");
}
@@ -1555,10 +1595,18 @@
const closePreview = p.close_preview || {};
const closeSheets = p.avail_pos != null && Number(p.avail_pos) > 0 ? p.avail_pos : p.pos;
const tickSz = p.tick_sz;
const premTxt = fmtDisplay(p.premium_paid_fmt, p.premium_paid != null ? fmtUsdc(p.premium_paid) : null);
const premCcy = posPremiumCcy(p);
const coinPos = isCoinPos(p);
const premTxt = fmtDisplay(
p.premium_paid_fmt,
p.premium_paid != null ? fmtPremiumAmt(p.premium_paid, premCcy) : null
);
// 优先用数值+tick 现算,避免接口侧 mark_px_fmt 带着浮点毛刺直出
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);
const netTxt = closePreview.bid_invalid || net == null
? "—"
: (fmtPremiumAmt(net, premCcy) + (coinPos ? (" " + premCcy) : ""));
return (
'<div class="pos-card-head">' +
'<div class="pos-card-symbol"><strong>' + (p.inst_id || "") + '</strong>' +
@@ -1577,21 +1625,21 @@
: "") +
"</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">' + premTxt + " " + premCcy + "</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>" +
'<div class="pos-cell"><span class="pos-label">净盈亏</span><span class="pos-value ' + uplCls + '">' +
(closePreview.bid_invalid || net == null ? "—" : fmt(net, 2)) + "</span></div>" +
netTxt + "</span></div>" +
'<div class="pos-cell"><span class="pos-label">收益率</span><span class="pos-value ' + uplCls + '">' +
(closePreview.bid_invalid || roi == null ? "—" : fmt(roi, 2) + "%") + "</span></div>" +
'<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, p.premium_paid)) + "</span></div>" +
: fmtClosePreview(closePreview, p.premium_paid, p)) + "</span></div>" +
"</div>" +
(function () {
const hint = closeGateHint(closePreview);
@@ -1628,6 +1676,7 @@
);
const statePe = String(p.profit_exit_state || (enabled ? "active" : "idle"));
const req = p.profit_exit_required_recycle;
const premCcy = posPremiumCcy(p);
let statusTxt = enabled ? ("监控中 · " + multLabel) : "未开启";
if (enabled && statePe === "closing") statusTxt = "平仓挂单中 · " + multLabel;
return (
@@ -1644,7 +1693,7 @@
'<span class="opt-target-armed">' + statusTxt + "</span>" +
'<span class="muted opt-target-row-hint">' +
(enabled
? ("1倍=盈利=权利金" + (req != null ? (" · 需回收≥" + fmtUsdc(req)) : ""))
? ("1倍=盈利=权利金" + (req != null ? (" · 需回收≥" + fmtPremiumAmt(req, premCcy) + " " + premCcy) : ""))
: "开启后自选倍数;达标按买一限价平;可随时关闭") +
"</span>" +
"</div>"
@@ -1659,16 +1708,23 @@
return null;
}
function formatTargetEstimateHtml(optType, strike, targetIdx, ethAmount, premiumPaid) {
function formatTargetEstimateHtml(optType, strike, targetIdx, ethAmount, premiumPaid, indexPx, p) {
const value = estimateExpiryValue(optType, strike, targetIdx, ethAmount);
const profit = estimateExpiryProfit(optType, strike, targetIdx, ethAmount, premiumPaid);
const rr = estimateProfitRr(profit, premiumPaid);
const profit = estimateExpiryProfit(optType, strike, targetIdx, ethAmount, premiumPaid, indexPx);
const rr = estimateProfitRr(profit, premiumPaid, indexPx);
if (value == null && profit == null && rr == null) return "";
const coinPos = isCoinPos(p);
const valueUnit = coinPos ? " U(估)" : " USDC";
const profitTxt = profit == null
? "—"
: (coinPos
? ((Number(profit) > 0 ? "+" : "") + fmtUsdc(profit) + " U(估)")
: fmtUsdcSigned(profit));
let html = '<span class="opt-target-est">';
html += '<span class="opt-target-est-item"><span class="k">价值</span><span class="v">' +
(value == null ? "—" : fmtUsdc(value) + " USDC") + "</span></span>";
(value == null ? "—" : fmtUsdc(value) + valueUnit) + "</span></span>";
html += '<span class="opt-target-est-item"><span class="k">预估盈利</span><span class="v ' + pnlCls(profit) + '">' +
(profit == null ? "—" : fmtUsdcSigned(profit)) + "</span></span>";
profitTxt + "</span></span>";
html += '<span class="opt-target-est-item"><span class="k">盈亏比</span><span class="v ' + pnlCls(rr) + '">' +
(rr == null ? "—" : fmtProfitRr(rr)) + "</span></span>";
html += "</span>";
@@ -1715,7 +1771,7 @@
const ethAmt = posEthAmount(p);
const prem = p.premium_paid;
const estHtml = armed
? formatTargetEstimateHtml(p.opt_type, p.strike, tgt, ethAmt, prem)
? formatTargetEstimateHtml(p.opt_type, p.strike, tgt, ethAmt, prem, p.idx_px, p)
: '<span class="opt-target-est opt-target-est--idle"></span>';
return (
'<div class="opt-target-row" data-inst="' + inst + '"' +
@@ -1723,6 +1779,8 @@
' data-strike="' + (p.strike != null ? p.strike : "") + '"' +
' data-eth="' + (ethAmt != null ? ethAmt : "") + '"' +
' data-prem="' + (prem != null ? prem : "") + '"' +
' data-idx="' + (p.idx_px != null ? p.idx_px : "") + '"' +
' data-prem-ccy="' + posPremiumCcy(p) + '"' +
' data-armed-target="' + (armed ? tgt : "") + '">' +
'<span class="opt-target-row-label">委托</span>' +
'<input type="number" class="opt-pos-target-input" data-inst="' + inst + '" step="0.1" min="0" placeholder="监控目标指数" value="' +
@@ -1758,7 +1816,9 @@
row.getAttribute("data-strike"),
targetRaw,
row.getAttribute("data-eth"),
row.getAttribute("data-prem")
row.getAttribute("data-prem"),
row.getAttribute("data-idx"),
{ premium_ccy: row.getAttribute("data-prem-ccy"), margin_mode: row.getAttribute("data-prem-ccy") === "USDC" ? "usdc" : "coin", inst_id: row.getAttribute("data-inst") }
);
if (!html) {
est.className = "opt-target-est opt-target-est--idle";
@@ -1804,7 +1864,8 @@
(expAttr
? '<span class="opt-pos-bar-cd">到期 <span class="opt-expiry-cd" data-opt-exp-ms="' + expAttr + '">—</span></span>'
: "") +
'<span class="opt-pos-bar-pnl ' + uplCls + '">' + (net == null ? "—" : fmt(net, 2) + " USDC") + "</span>" +
'<span class="opt-pos-bar-pnl ' + uplCls + '">' +
(net == null ? "—" : (fmtPremiumAmt(net, posPremiumCcy(p)) + " " + posPremiumCcy(p))) + "</span>" +
'<span class="opt-pos-bar-roi ' + uplCls + '">' +
(roi == null ? "—" : fmt(roi, 2) + "%") + "</span>" +
"</span>" +
@@ -2060,12 +2121,20 @@
return;
}
const lv = (preview.levels && preview.levels[0]) || {};
const posLike = {
inst_id: inst,
premium_ccy: q.premium_ccy || (preview.close_gate && preview.close_gate.premium_ccy) || null,
margin_mode: q.options_margin_mode || q.margin_mode || null,
};
const premCcy = posPremiumCcy(posLike);
const msg = [
"按买一限价卖出本轮可平张数?",
"合约: " + inst,
"锁定买一: " + (lv.px != null ? lv.px : "—") + " × " + (lv.sheets != null ? lv.sheets : preview.covered_sheets) + " 张",
"预计收回: " + fmtClosePreviewText(preview),
preview.estimated_pnl != null ? "预估盈亏: " + fmt(preview.estimated_pnl, 4) + " USDC" : "",
"预计收回: " + fmtClosePreviewText(preview, posLike),
preview.estimated_pnl != null
? ("预估盈亏: " + fmtPremiumAmtSigned(preview.estimated_pnl, premCcy))
: "",
preview.uncovered_sheets > 0 ? "\n注意: 买一深度不足,预计仍剩 " + preview.uncovered_sheets + " 张,需下次再平。" : ""
].filter(function (x) { return x !== ""; }).join("\n");
if (!confirm(msg)) return;
@@ -2079,7 +2148,9 @@
if (r.ok) {
let okMsg = "买一平仓已提交 " + (r.submitted_sheets || 0) + " 张";
if (r.locked_bid_px != null) okMsg += "\n锁定买一: " + r.locked_bid_px;
if (r.premium_received != null) okMsg += "\n预估收回: " + fmt(r.premium_received, 4) + " USDC";
if (r.premium_received != null) {
okMsg += "\n预估收回: " + fmtPremiumAmt(r.premium_received, premCcy) + " " + premCcy;
}
if (r.remaining_sheets > 0) okMsg += "\n剩余: " + r.remaining_sheets + " 张(下次再平)";
if (r.stopped_reason) okMsg += "\n状态: " + r.stopped_reason;
alert(okMsg);
+45 -10
View File
@@ -34,6 +34,28 @@
return Number(v).toFixed(2);
}
function posPremiumCcy(p) {
const ccy = String((p && p.premium_ccy) || "").trim().toUpperCase();
if (ccy) return ccy;
const mode = String((p && p.margin_mode) || "").toLowerCase();
const inst = String((p && p.inst_id) || "");
if (mode === "coin" || (inst.indexOf("-USD-") >= 0 && inst.indexOf("_UM") < 0)) {
return (inst.split("-")[0] || "ETH").toUpperCase() || "ETH";
}
return "USDC";
}
function fmtPremiumAmt(v, ccy) {
if (v === null || v === undefined || Number.isNaN(Number(v))) return "—";
const n = Number(v);
const unit = String(ccy || "USDC").toUpperCase();
if (unit === "ETH" || unit === "BTC") {
let s = n.toFixed(8).replace(/\.?0+$/, "");
return s || "0";
}
return fmtUsdc(n);
}
function optTypeLabel(t) {
return (t || "").toUpperCase() === "P" ? "看跌 Put" : "看涨 Call";
}
@@ -136,9 +158,10 @@
return (net / prem) * 100;
}
function fmtClosePreview(preview, premiumPaid, hub) {
function fmtClosePreview(preview, premiumPaid, hub, p) {
if (!preview || preview.total_received == null) return "—";
const recvTxt = fmtUsdc(preview.total_received);
const ccy = posPremiumCcy(p);
const recvTxt = fmtPremiumAmt(preview.total_received, ccy);
let cls = "";
const prem = Number(premiumPaid);
const recv = Number(preview.total_received);
@@ -146,7 +169,7 @@
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>";
return '<span class="opt-close-value' + cls + '">' + recvTxt + " " + ccy + "</span>";
}
function expiryCdHtml(expMs) {
@@ -168,7 +191,11 @@
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 premCcy = posPremiumCcy(p);
const premTxt = fmtDisplay(
p.premium_paid_fmt,
p.premium_paid != null ? fmtPremiumAmt(p.premium_paid, premCcy) : 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 = "";
@@ -182,7 +209,7 @@
const pnlCells = hidePnl
? ""
: '<div class="pos-cell"><span class="pos-label">净盈亏</span><span class="pos-value ' + uplCls + '">' +
(net == null ? "—" : fmt(net, 2)) + "</span></div>" +
(net == null ? "—" : (fmtPremiumAmt(net, premCcy) + (premCcy !== "USDC" ? (" " + premCcy) : ""))) + "</span></div>" +
'<div class="pos-cell"><span class="pos-label">收益率</span><span class="pos-value ' + uplCls + '">' +
(roi == null ? "—" : fmt(roi, 2) + "%") + "</span></div>";
return (
@@ -202,7 +229,7 @@
: "") +
"</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">' + premTxt + " " + premCcy + "</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>" +
@@ -213,7 +240,7 @@
'<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>" +
: fmtClosePreview(closePreview, hidePnl ? null : p.premium_paid, hub, p)) + "</span></div>" +
"</div>" +
(function () {
const hint = closeGateHint(closePreview);
@@ -226,6 +253,7 @@
const strike = Number(p.strike);
const tgt = Number(p.target_index);
const prem = Number(p.premium_paid);
const idx = Number(p.idx_px);
let profit = null;
let value = null;
if (Number.isFinite(tgt) && Number.isFinite(strike) && eth > 0) {
@@ -233,10 +261,17 @@
const intrinsic = o === "C" ? Math.max(0, tgt - strike) : o === "P" ? Math.max(0, strike - tgt) : null;
if (intrinsic != null) {
value = Math.round(intrinsic * eth * 100) / 100;
if (!hidePnl && Number.isFinite(prem)) profit = Math.round((value - prem) * 100) / 100;
if (!hidePnl && Number.isFinite(prem)) {
let premUsd = prem;
if (premCcy !== "USDC" && Number.isFinite(idx) && idx > 0) premUsd = prem * idx;
profit = Math.round((value - premUsd) * 100) / 100;
}
}
}
const profitTxt = profit == null ? "—" : ((profit > 0 ? "+" : "") + fmtUsdc(profit) + " USDC");
const valueUnit = premCcy !== "USDC" ? " U(估)" : " USDC";
const profitTxt = profit == null
? "—"
: ((profit > 0 ? "+" : "") + fmtUsdc(profit) + (premCcy !== "USDC" ? " U(估)" : " USDC"));
const profitCls = profit > 0 ? " pnl-pos" : profit < 0 ? " pnl-neg" : "";
const hedgeTarget = p.hedge_plan_target || null;
const managed = hedgeTarget && hedgeTarget.managed_by === "hedge_plan";
@@ -247,7 +282,7 @@
'<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">目标 ' + fmt(p.target_index, 1) + "</span>" +
'<span class="pos-value">价值 ' + (value == null ? "—" : fmtUsdc(value) + " USDC") + "</span>" +
'<span class="pos-value">价值 ' + (value == null ? "—" : fmtUsdc(value) + valueUnit) + "</span>" +
profitSpan +
'<span class="muted opt-target-row-hint">' +
(managed ? "进行中 · 由对冲计划监控,到位后仅平盈利腿" : "监控中 · 到位按买一限价平") +
+29 -1
View File
@@ -171,6 +171,21 @@ def format_usdc_amount(v: float | None) -> str | None:
return f"{float(v):.2f}"
def format_premium_amount(v: float | None, *, ccy: str | None = "USDC") -> str | None:
"""权利金/回收金额文案:USDC 2 位;币本位 ETH/BTC 最多 8 位去尾零."""
if v is None:
return None
try:
n = float(v)
except (TypeError, ValueError):
return None
unit = (ccy or "USDC").strip().upper() or "USDC"
if unit in ("ETH", "BTC"):
txt = f"{n:.8f}".rstrip("0").rstrip(".")
return txt or "0"
return f"{n:.2f}"
def is_option_full_close_history(raw: dict[str, Any]) -> bool:
"""仅保留 OKX 历史仓位中的「全部平仓/强平/ADL 全平」记录,排除部分平仓."""
close_type = str(raw.get("type") or "").strip()
@@ -1797,6 +1812,16 @@ def format_position_row(
ct_mult=ct_mult,
)
exp_time_ms = normalize_option_exp_ms(pos.get("expTime"), inst_id)
try:
from lib.options.options_margin_mode_lib import margin_mode_from_inst_id, premium_ccy_for_mode
row_mode = margin_mode_from_inst_id(inst_id) if inst_id else "usdc"
underly = (inst_id.split("-")[0] if inst_id else "ETH") or "ETH"
premium_ccy = premium_ccy_for_mode(row_mode, underly)
except Exception:
row_mode = "usdc"
underly = (inst_id.split("-")[0] if inst_id else "ETH") or "ETH"
premium_ccy = "USDC"
return {
"inst_id": inst_id or pos.get("instId"),
"pos": sheets,
@@ -1805,11 +1830,14 @@ def format_position_row(
"mark_px": mark,
"avg_px_fmt": format_option_px(avg, tick_sz) if avg is not None else None,
"mark_px_fmt": format_option_px(mark, tick_sz) if mark is not None else None,
"premium_paid_fmt": format_usdc_amount(premium_paid),
"premium_paid_fmt": format_premium_amount(premium_paid, ccy=premium_ccy),
"tick_sz": tick_sz,
"ct_mult": ct_mult,
"idx_px": idx_px,
"premium_paid": premium_paid,
"margin_mode": row_mode,
"premium_ccy": premium_ccy,
"underlying": underly,
"upl": upl,
"upl_ratio_pct": round(upl_ratio * 100, 2) if upl_ratio is not None else None,
"exp_time": exp_time_ms,
+41 -6
View File
@@ -60,6 +60,14 @@ def is_close_gate_passed(inst_id: str) -> bool:
return bool((_gates.get(inst) or {}).get("passed"))
def _fmt_gate_amt(v: float, *, ccy: str) -> str:
unit = (ccy or "USDC").strip().upper() or "USDC"
if unit in ("ETH", "BTC"):
txt = f"{float(v):.8f}".rstrip("0").rstrip(".")
return txt or "0"
return f"{float(v):.4f}"
def update_close_gate(
inst_id: str,
*,
@@ -68,6 +76,7 @@ def update_close_gate(
now: float | None = None,
min_mult: float | None = None,
hold_seconds: float | None = None,
premium_ccy: str | None = None,
) -> dict[str, Any]:
"""
根据当前买盘可回收金额刷新门控.
@@ -89,9 +98,16 @@ def update_close_gate(
if hold < 0:
hold = 0.0
with _lock:
prev_ccy = (_gates.get(inst) or {}).get("premium_ccy")
ccy = (premium_ccy or prev_ccy or "USDC").strip().upper() or "USDC"
if ccy not in ("ETH", "BTC", "USDC"):
ccy = "USDC"
need_decimals = 8 if ccy in ("ETH", "BTC") else 4
prem = _safe_float(premium_paid)
recv = _safe_float(recycle_usdc)
need = round(prem * mult, 4) if prem is not None and prem > 0 else None
need = round(prem * mult, need_decimals) if prem is not None and prem > 0 else None
recycle_ok = bool(
prem is not None and prem > 0 and recv is not None and need is not None and recv + 1e-12 >= need
)
@@ -117,6 +133,7 @@ def update_close_gate(
"min_mult": mult,
"hold_seconds": hold,
"passed": passed,
"premium_ccy": ccy,
}
_gates[inst] = state
@@ -126,10 +143,13 @@ def update_close_gate(
elif recv is None:
msg = "暂无有效买盘可回收金额"
elif not recycle_ok:
msg = f"可回收 {recv:.4f} USDC < 权利金×{mult:g}({need:.4f}),目标平仓门控未过"
msg = (
f"可回收 {_fmt_gate_amt(recv, ccy=ccy)} {ccy} < 权利金×{mult:g}"
f"({_fmt_gate_amt(need, ccy=ccy)}),目标平仓门控未过"
)
elif not ready:
msg = (
f"可回收已达×{mult:g}({recv:.4f}/{need:.4f}),"
f"可回收已达×{mult:g}({_fmt_gate_amt(recv, ccy=ccy)}/{_fmt_gate_amt(need, ccy=ccy)} {ccy}),"
f"需再持续 {remain:.0f}s(已 {held:.0f}/{hold:.0f}s)门控才通过"
)
else:
@@ -144,6 +164,7 @@ def update_close_gate(
"recycle_usdc": recv,
"premium_paid": prem,
"need_recycle_usdc": need,
"premium_ccy": ccy,
"min_mult": mult,
"hold_seconds": hold,
"held_seconds": round(held, 1) if recycle_ok else 0.0,
@@ -160,25 +181,39 @@ def check_close_gate(
*,
recycle_usdc: float | None = None,
premium_paid: float | None = None,
premium_ccy: str | None = None,
refresh: bool = True,
) -> dict[str, Any]:
"""检查是否允许平仓;默认先用最新回收/权利金刷新."""
inst = (inst_id or "").strip()
if refresh:
if recycle_usdc is None or premium_paid is None:
if recycle_usdc is None or premium_paid is None or premium_ccy is None:
with _lock:
prev = _gates.get(inst) or {}
if recycle_usdc is None:
recycle_usdc = prev.get("recycle")
if premium_paid is None:
premium_paid = prev.get("premium")
return update_close_gate(inst, recycle_usdc=recycle_usdc, premium_paid=premium_paid)
if premium_ccy is None:
premium_ccy = prev.get("premium_ccy")
return update_close_gate(
inst,
recycle_usdc=recycle_usdc,
premium_paid=premium_paid,
premium_ccy=premium_ccy,
)
with _lock:
prev = _gates.get(inst)
if not prev:
return update_close_gate(inst, recycle_usdc=recycle_usdc, premium_paid=premium_paid)
return update_close_gate(
inst,
recycle_usdc=recycle_usdc,
premium_paid=premium_paid,
premium_ccy=premium_ccy,
)
return update_close_gate(
inst,
recycle_usdc=recycle_usdc if recycle_usdc is not None else prev.get("recycle"),
premium_paid=premium_paid if premium_paid is not None else prev.get("premium"),
premium_ccy=premium_ccy if premium_ccy is not None else prev.get("premium_ccy"),
)
+2 -1
View File
@@ -134,7 +134,8 @@ def sum_open_premium_paid(conn: sqlite3.Connection, inst_id: str) -> float | Non
).fetchone()
if not row or int(row["n"] or 0) < 1:
return None
return round(float(row["total"] or 0), 4)
# 币本位权利金常 <1e-4,保留 8 位避免被裁成 0
return round(float(row["total"] or 0), 8)
def sum_open_sheets(conn: sqlite3.Connection, inst_id: str) -> int | None:
+9 -2
View File
@@ -14,14 +14,21 @@ def enrich_position_row_display(
meta_cache: dict[str, dict[str, Any] | None] | None = None,
premium_override: float | None = None,
) -> dict[str, Any]:
from lib.exchange.okx_options_lib import format_position_row, format_usdc_amount, tick_sz_and_ct_mult
from lib.exchange.okx_options_lib import format_position_row, format_premium_amount, tick_sz_and_ct_mult
from lib.options.options_margin_mode_lib import margin_mode_from_inst_id, premium_ccy_for_mode
inst_id = str(raw_pos.get("instId") or "").strip()
tick_sz, ct_mult = tick_sz_and_ct_mult(ex, inst_id, meta_cache)
row = format_position_row(raw_pos, ct_mult=ct_mult, tick_sz=tick_sz)
row_mode = margin_mode_from_inst_id(inst_id) if inst_id else "usdc"
underly = str(row.get("underlying") or (inst_id.split("-")[0] if inst_id else "ETH") or "ETH")
premium_ccy = premium_ccy_for_mode(row_mode, underly)
row["margin_mode"] = row_mode
row["premium_ccy"] = premium_ccy
row["margin_mode_label"] = "币本位" if row_mode == "coin" else "USDC"
if premium_override is not None:
row["premium_paid"] = premium_override
row["premium_paid_fmt"] = format_usdc_amount(premium_override)
row["premium_paid_fmt"] = format_premium_amount(row.get("premium_paid"), ccy=premium_ccy)
return row
+5 -1
View File
@@ -51,9 +51,12 @@ def attach_close_preview(
intrinsic_px=intrinsic,
max_levels=1,
)
premium_ccy = str(row.get("premium_ccy") or "USDC").strip().upper() or "USDC"
# 残档时不累计 2×门控;有效买一时刷新计时(仅自动平仓需要)
if preview.get("bid_invalid") or preview.get("auto_close_blocked"):
gate = update_close_gate(inst_id, recycle_usdc=None, premium_paid=paid)
gate = update_close_gate(
inst_id, recycle_usdc=None, premium_paid=paid, premium_ccy=premium_ccy
)
preview["close_gate"] = gate
preview["close_gate_blocked"] = True
preview["close_gate_msg"] = preview.get("bid_invalid_reason") or gate.get("msg")
@@ -64,6 +67,7 @@ def attach_close_preview(
inst_id,
recycle_usdc=_safe_float(preview.get("total_received")),
premium_paid=paid,
premium_ccy=premium_ccy,
)
passed = bool(gate.get("passed") or is_close_gate_passed(inst_id) or gate.get("ready"))
preview["close_gate"] = gate
+15 -1
View File
@@ -533,7 +533,21 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
if mode == "close_preview":
paid = _open_premium_paid(cfg, inst_id)
target = sheet_count if sheet_count is not None else 0
return jsonify(_attach_close_preview(cfg, ex, {**q, "pos": target, "premium_paid": paid}, sheets=target, premium_paid=paid))
from lib.options.options_margin_mode_lib import margin_mode_from_inst_id, premium_ccy_for_mode
row_mode = margin_mode_from_inst_id(inst_id)
prem_ccy = premium_ccy_for_mode(row_mode, (inst_id.split("-")[0] if inst_id else "ETH"))
preview_row = {
**q,
"pos": target,
"premium_paid": paid,
"margin_mode": row_mode,
"premium_ccy": prem_ccy,
}
out = _attach_close_preview(cfg, ex, preview_row, sheets=target, premium_paid=paid)
out["options_margin_mode"] = row_mode
out["premium_ccy"] = prem_ccy
return jsonify(out)
mode, mode_note = _normalize_size_mode(mode)
# 币本位:报价预览走 USDT 预算→估币→张数,禁止再查 USDC