Add live expiry countdown for options positions on instance page and trading hub.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2890,6 +2890,16 @@ html[data-theme="light"] .options-estimate-row {
|
||||
.opt-pos-card {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.opt-expiry-cd {
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-weight: 600;
|
||||
}
|
||||
.opt-expiry-cd--urgent {
|
||||
color: #ffb347;
|
||||
}
|
||||
.opt-expiry-cd--expired {
|
||||
color: var(--muted);
|
||||
}
|
||||
.settings-card--compact .options-settings-section {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 期权到期倒计时(实例期权页 + 中控监控/看板共用)
|
||||
*/
|
||||
(function (global) {
|
||||
function normalizeExpMs(v) {
|
||||
if (v == null || v === "") return null;
|
||||
var n = Number(v);
|
||||
if (!Number.isFinite(n) || n <= 0) return null;
|
||||
if (n < 1e12) n *= 1000;
|
||||
return n;
|
||||
}
|
||||
|
||||
function formatCountdown(expMs, nowMs) {
|
||||
var ms = normalizeExpMs(expMs);
|
||||
if (ms == null) return "—";
|
||||
var now = nowMs != null ? nowMs : Date.now();
|
||||
var rem = Math.max(0, Math.floor((ms - now) / 1000));
|
||||
if (rem <= 0) return "已到期";
|
||||
var d = Math.floor(rem / 86400);
|
||||
var h = Math.floor((rem % 86400) / 3600);
|
||||
var m = Math.floor((rem % 3600) / 60);
|
||||
var s = rem % 60;
|
||||
var pad = function (x) {
|
||||
return String(x).padStart(2, "0");
|
||||
};
|
||||
if (d > 0) return d + "天 " + pad(h) + ":" + pad(m) + ":" + pad(s);
|
||||
return pad(h) + ":" + pad(m) + ":" + pad(s);
|
||||
}
|
||||
|
||||
function tick(root) {
|
||||
var scope = root && root.querySelectorAll ? root : document;
|
||||
var now = Date.now();
|
||||
scope.querySelectorAll("[data-opt-exp-ms]").forEach(function (el) {
|
||||
var exp = el.getAttribute("data-opt-exp-ms");
|
||||
var text = formatCountdown(exp, now);
|
||||
el.textContent = text;
|
||||
var expMs = normalizeExpMs(exp);
|
||||
el.classList.toggle("opt-expiry-cd--urgent", expMs != null && expMs - now > 0 && expMs - now < 3600000);
|
||||
el.classList.toggle("opt-expiry-cd--expired", text === "已到期");
|
||||
});
|
||||
}
|
||||
|
||||
var timer = null;
|
||||
function ensureTimer() {
|
||||
tick();
|
||||
if (timer) return;
|
||||
timer = setInterval(function () {
|
||||
tick();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
global.OptionsExpiryCountdown = {
|
||||
normalizeExpMs: normalizeExpMs,
|
||||
format: formatCountdown,
|
||||
tick: tick,
|
||||
ensureTimer: ensureTimer,
|
||||
};
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
@@ -431,6 +431,8 @@
|
||||
const upl = p.upl;
|
||||
const uplCls = upl > 0 ? "pos-pnl-profit" : upl < 0 ? "pos-pnl-loss" : "";
|
||||
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) : "";
|
||||
return (
|
||||
'<div class="pos-card opt-pos-card" data-inst="' + (p.inst_id || "") + '">' +
|
||||
'<div class="pos-card-head">' +
|
||||
@@ -442,6 +444,9 @@
|
||||
'<div class="pos-meta">' +
|
||||
'<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">到期倒计时: <span class="opt-expiry-cd" data-opt-exp-ms="' + expAttr + '">—</span></span>'
|
||||
: "") +
|
||||
"</div>" +
|
||||
'<div class="pos-grid">' +
|
||||
'<div class="pos-cell"><span class="pos-label">权利金</span><span class="pos-value">' + fmt(p.premium_paid, 4) + " USDC</span></div>" +
|
||||
@@ -509,6 +514,9 @@
|
||||
closePosition(btn.getAttribute("data-inst"), btn);
|
||||
});
|
||||
});
|
||||
if (window.OptionsExpiryCountdown && OptionsExpiryCountdown.ensureTimer) {
|
||||
OptionsExpiryCountdown.ensureTimer();
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshStats() {
|
||||
|
||||
@@ -227,6 +227,35 @@ def option_fields_from_inst_id(inst_id: str) -> tuple[str | None, float | None]:
|
||||
return opt_type, strike
|
||||
|
||||
|
||||
def expiry_ms_from_inst_id(inst_id: str) -> int | None:
|
||||
"""从 instId 日期段解析到期时刻(OKX 期权默认 08:00 UTC)。"""
|
||||
parts = (inst_id or "").strip().split("-")
|
||||
if len(parts) < 3:
|
||||
return None
|
||||
date_part = parts[-3]
|
||||
if not re.fullmatch(r"\d{6}", date_part):
|
||||
return None
|
||||
try:
|
||||
from datetime import datetime, timezone
|
||||
|
||||
yy, mm, dd = int(date_part[0:2]), int(date_part[2:4]), int(date_part[4:6])
|
||||
dt = datetime(2000 + yy, mm, dd, 8, 0, 0, tzinfo=timezone.utc)
|
||||
return int(dt.timestamp() * 1000)
|
||||
except (ValueError, OSError):
|
||||
return None
|
||||
|
||||
|
||||
def normalize_option_exp_ms(exp_time: Any, inst_id: str = "") -> int | None:
|
||||
"""统一期权到期毫秒时间戳(优先 API expTime,否则从 instId 推算)。"""
|
||||
raw = _safe_float(exp_time)
|
||||
if raw is not None and raw > 0:
|
||||
ms = int(raw)
|
||||
if ms < 10_000_000_000:
|
||||
ms *= 1000
|
||||
return ms
|
||||
return expiry_ms_from_inst_id(inst_id)
|
||||
|
||||
|
||||
def fetch_option_instrument_meta(ex: ccxt.okx, inst_id: str) -> dict[str, Any] | None:
|
||||
family = inst_family_from_inst_id(inst_id)
|
||||
if not family:
|
||||
@@ -839,6 +868,7 @@ def format_position_row(pos: dict[str, Any], ct_mult: float = 0.01) -> dict[str,
|
||||
pos=sheets,
|
||||
ct_mult=ct_mult,
|
||||
)
|
||||
exp_time_ms = normalize_option_exp_ms(pos.get("expTime"), inst_id)
|
||||
return {
|
||||
"inst_id": inst_id or pos.get("instId"),
|
||||
"pos": sheets,
|
||||
@@ -849,7 +879,8 @@ def format_position_row(pos: dict[str, Any], ct_mult: float = 0.01) -> dict[str,
|
||||
"premium_paid": premium_paid,
|
||||
"upl": upl,
|
||||
"upl_ratio_pct": round(upl_ratio * 100, 2) if upl_ratio is not None else None,
|
||||
"exp_time": pos.get("expTime"),
|
||||
"exp_time": exp_time_ms,
|
||||
"exp_time_ms": exp_time_ms,
|
||||
"opt_type": opt_type,
|
||||
"strike": strike,
|
||||
"avail_pos": _safe_float(pos.get("availPos")),
|
||||
|
||||
@@ -136,4 +136,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/options_panel.js?v=12"></script>
|
||||
<script src="/static/options_expiry_countdown.js?v=1"></script>
|
||||
<script src="/static/options_panel.js?v=13"></script>
|
||||
|
||||
Reference in New Issue
Block a user