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:
@@ -8936,6 +8936,7 @@ _AI_REVIEW_RENDER_JS = os.path.join(_REPO_STATIC_DIR, "ai_review_render.js")
|
|||||||
_FORM_SUBMIT_GUARD_JS = os.path.join(_REPO_STATIC_DIR, "form_submit_guard.js")
|
_FORM_SUBMIT_GUARD_JS = os.path.join(_REPO_STATIC_DIR, "form_submit_guard.js")
|
||||||
_MANUAL_ORDER_RR_PREVIEW_JS = os.path.join(_REPO_STATIC_DIR, "manual_order_rr_preview.js")
|
_MANUAL_ORDER_RR_PREVIEW_JS = os.path.join(_REPO_STATIC_DIR, "manual_order_rr_preview.js")
|
||||||
_OPTIONS_PANEL_JS = os.path.join(_REPO_STATIC_DIR, "options_panel.js")
|
_OPTIONS_PANEL_JS = os.path.join(_REPO_STATIC_DIR, "options_panel.js")
|
||||||
|
_OPTIONS_EXPIRY_COUNTDOWN_JS = os.path.join(_REPO_STATIC_DIR, "options_expiry_countdown.js")
|
||||||
_OPTIONS_SETTINGS_JS = os.path.join(_REPO_STATIC_DIR, "options_settings.js")
|
_OPTIONS_SETTINGS_JS = os.path.join(_REPO_STATIC_DIR, "options_settings.js")
|
||||||
|
|
||||||
|
|
||||||
@@ -8967,6 +8968,13 @@ def static_options_panel_js():
|
|||||||
return send_file(_OPTIONS_PANEL_JS, mimetype="application/javascript; charset=utf-8")
|
return send_file(_OPTIONS_PANEL_JS, mimetype="application/javascript; charset=utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/static/options_expiry_countdown.js")
|
||||||
|
def static_options_expiry_countdown_js():
|
||||||
|
if not os.path.isfile(_OPTIONS_EXPIRY_COUNTDOWN_JS):
|
||||||
|
return Response("not found", status=404, mimetype="text/plain; charset=utf-8")
|
||||||
|
return send_file(_OPTIONS_EXPIRY_COUNTDOWN_JS, mimetype="application/javascript; charset=utf-8")
|
||||||
|
|
||||||
|
|
||||||
@app.route("/static/options_settings.js")
|
@app.route("/static/options_settings.js")
|
||||||
def static_options_settings_js():
|
def static_options_settings_js():
|
||||||
if not os.path.isfile(_OPTIONS_SETTINGS_JS):
|
if not os.path.isfile(_OPTIONS_SETTINGS_JS):
|
||||||
|
|||||||
@@ -2890,6 +2890,16 @@ html[data-theme="light"] .options-estimate-row {
|
|||||||
.opt-pos-card {
|
.opt-pos-card {
|
||||||
margin-bottom: 10px;
|
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 {
|
.settings-card--compact .options-settings-section {
|
||||||
margin-bottom: 8px;
|
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 upl = p.upl;
|
||||||
const uplCls = upl > 0 ? "pos-pnl-profit" : upl < 0 ? "pos-pnl-loss" : "";
|
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 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 (
|
return (
|
||||||
'<div class="pos-card opt-pos-card" data-inst="' + (p.inst_id || "") + '">' +
|
'<div class="pos-card opt-pos-card" data-inst="' + (p.inst_id || "") + '">' +
|
||||||
'<div class="pos-card-head">' +
|
'<div class="pos-card-head">' +
|
||||||
@@ -442,6 +444,9 @@
|
|||||||
'<div class="pos-meta">' +
|
'<div class="pos-meta">' +
|
||||||
'<span class="pos-meta-item">行权价: ' + fmt(p.strike, 0) + "</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>" +
|
'<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>" +
|
||||||
'<div class="pos-grid">' +
|
'<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>" +
|
'<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);
|
closePosition(btn.getAttribute("data-inst"), btn);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
if (window.OptionsExpiryCountdown && OptionsExpiryCountdown.ensureTimer) {
|
||||||
|
OptionsExpiryCountdown.ensureTimer();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshStats() {
|
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
|
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:
|
def fetch_option_instrument_meta(ex: ccxt.okx, inst_id: str) -> dict[str, Any] | None:
|
||||||
family = inst_family_from_inst_id(inst_id)
|
family = inst_family_from_inst_id(inst_id)
|
||||||
if not family:
|
if not family:
|
||||||
@@ -839,6 +868,7 @@ def format_position_row(pos: dict[str, Any], ct_mult: float = 0.01) -> dict[str,
|
|||||||
pos=sheets,
|
pos=sheets,
|
||||||
ct_mult=ct_mult,
|
ct_mult=ct_mult,
|
||||||
)
|
)
|
||||||
|
exp_time_ms = normalize_option_exp_ms(pos.get("expTime"), inst_id)
|
||||||
return {
|
return {
|
||||||
"inst_id": inst_id or pos.get("instId"),
|
"inst_id": inst_id or pos.get("instId"),
|
||||||
"pos": sheets,
|
"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,
|
"premium_paid": premium_paid,
|
||||||
"upl": upl,
|
"upl": upl,
|
||||||
"upl_ratio_pct": round(upl_ratio * 100, 2) if upl_ratio is not None else None,
|
"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,
|
"opt_type": opt_type,
|
||||||
"strike": strike,
|
"strike": strike,
|
||||||
"avail_pos": _safe_float(pos.get("availPos")),
|
"avail_pos": _safe_float(pos.get("availPos")),
|
||||||
|
|||||||
@@ -136,4 +136,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
||||||
|
|||||||
@@ -771,6 +771,7 @@ _TRADE_STATS_CALENDAR_CSS = _REPO_STATIC / "trade_stats_calendar.css"
|
|||||||
_TRADE_STATS_CALENDAR_JS = _REPO_STATIC / "trade_stats_calendar.js"
|
_TRADE_STATS_CALENDAR_JS = _REPO_STATIC / "trade_stats_calendar.js"
|
||||||
_ACCOUNT_RISK_BADGE_CSS = _REPO_STATIC / "account_risk_badge.css"
|
_ACCOUNT_RISK_BADGE_CSS = _REPO_STATIC / "account_risk_badge.css"
|
||||||
_ACCOUNT_RISK_BADGE_JS = _REPO_STATIC / "account_risk_badge.js"
|
_ACCOUNT_RISK_BADGE_JS = _REPO_STATIC / "account_risk_badge.js"
|
||||||
|
_OPTIONS_EXPIRY_COUNTDOWN_JS = _REPO_STATIC / "options_expiry_countdown.js"
|
||||||
|
|
||||||
|
|
||||||
@app.get("/assets/account_risk_badge.css")
|
@app.get("/assets/account_risk_badge.css")
|
||||||
@@ -795,6 +796,16 @@ def hub_account_risk_badge_js():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/assets/options_expiry_countdown.js")
|
||||||
|
def hub_options_expiry_countdown_js():
|
||||||
|
if not _OPTIONS_EXPIRY_COUNTDOWN_JS.is_file():
|
||||||
|
raise HTTPException(status_code=404, detail="options_expiry_countdown.js not found")
|
||||||
|
return FileResponse(
|
||||||
|
str(_OPTIONS_EXPIRY_COUNTDOWN_JS),
|
||||||
|
media_type="application/javascript; charset=utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/assets/ai_review_render.js")
|
@app.get("/assets/ai_review_render.js")
|
||||||
def hub_ai_review_render_js():
|
def hub_ai_review_render_js():
|
||||||
"""与三所实例共用仓库根 static/ai_review_render.js(须在 /assets mount 之前注册)。"""
|
"""与三所实例共用仓库根 static/ai_review_render.js(须在 /assets mount 之前注册)。"""
|
||||||
|
|||||||
@@ -8161,6 +8161,20 @@ html[data-theme="light"] .hub-monitor-options .hub-monitor-block-label {
|
|||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.opt-expiry-cd {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opt-expiry-cd--urgent {
|
||||||
|
color: #ffb347;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opt-expiry-cd--expired {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
html[data-theme="light"] .hub-options-table th {
|
html[data-theme="light"] .hub-options-table th {
|
||||||
color: #3a5068;
|
color: #3a5068;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2250,6 +2250,9 @@
|
|||||||
TimeCloseUI.tickLocalCountdowns();
|
TimeCloseUI.tickLocalCountdowns();
|
||||||
}
|
}
|
||||||
ensureHubHoldDurationTimer();
|
ensureHubHoldDurationTimer();
|
||||||
|
if (window.OptionsExpiryCountdown && OptionsExpiryCountdown.ensureTimer) {
|
||||||
|
OptionsExpiryCountdown.ensureTimer();
|
||||||
|
}
|
||||||
|
|
||||||
if (expandedExchangeId && fs && fsInner) {
|
if (expandedExchangeId && fs && fsInner) {
|
||||||
const row = rows.find((r) => String(r.id) === String(expandedExchangeId));
|
const row = rows.find((r) => String(r.id) === String(expandedExchangeId));
|
||||||
@@ -2264,6 +2267,9 @@
|
|||||||
TimeCloseUI.tickLocalCountdowns();
|
TimeCloseUI.tickLocalCountdowns();
|
||||||
}
|
}
|
||||||
ensureHubHoldDurationTimer();
|
ensureHubHoldDurationTimer();
|
||||||
|
if (window.OptionsExpiryCountdown && OptionsExpiryCountdown.ensureTimer) {
|
||||||
|
OptionsExpiryCountdown.ensureTimer();
|
||||||
|
}
|
||||||
fsInner.querySelectorAll(".btn-expand-back").forEach((btn) => {
|
fsInner.querySelectorAll(".btn-expand-back").forEach((btn) => {
|
||||||
btn.onclick = (ev) => {
|
btn.onclick = (ev) => {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
@@ -3472,10 +3478,16 @@
|
|||||||
return s.slice(0, 10) + "…" + s.slice(-8);
|
return s.slice(0, 10) + "…" + s.slice(-8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function optionsExpiryCdHtml(expMs) {
|
||||||
|
const ms = expMs != null && expMs !== "" ? String(expMs) : "";
|
||||||
|
if (!ms) return "—";
|
||||||
|
return `<span class="opt-expiry-cd" data-opt-exp-ms="${esc(ms)}">—</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
function renderOptionsPositionsTable(pos) {
|
function renderOptionsPositionsTable(pos) {
|
||||||
if (!pos.length) return '<div class="empty-hint">暂无期权持仓</div>';
|
if (!pos.length) return '<div class="empty-hint">暂无期权持仓</div>';
|
||||||
let html = '<div class="table-wrap hub-options-table-wrap"><table class="hub-options-table"><thead><tr>';
|
let html = '<div class="table-wrap hub-options-table-wrap"><table class="hub-options-table"><thead><tr>';
|
||||||
html += "<th>合约</th><th>类型</th><th>张数</th><th>指数</th><th>到期平衡</th><th>平掉回本</th><th>浮盈</th><th>浮盈%</th>";
|
html += "<th>合约</th><th>类型</th><th>张数</th><th>到期倒计时</th><th>指数</th><th>到期平衡</th><th>平掉回本</th><th>浮盈</th><th>浮盈%</th>";
|
||||||
html += "</tr></thead><tbody>";
|
html += "</tr></thead><tbody>";
|
||||||
pos.forEach((p) => {
|
pos.forEach((p) => {
|
||||||
const optType = (p.opt_type || "").toUpperCase() === "C" ? "Call" : (p.opt_type || "").toUpperCase() === "P" ? "Put" : (p.opt_type || "—");
|
const optType = (p.opt_type || "").toUpperCase() === "C" ? "Call" : (p.opt_type || "").toUpperCase() === "P" ? "Put" : (p.opt_type || "—");
|
||||||
@@ -3483,6 +3495,7 @@
|
|||||||
<td><code class="hub-options-inst" title="${esc(p.inst_id || "")}">${esc(shortOptionsInst(p.inst_id))}</code></td>
|
<td><code class="hub-options-inst" title="${esc(p.inst_id || "")}">${esc(shortOptionsInst(p.inst_id))}</code></td>
|
||||||
<td>${esc(optType)}</td>
|
<td>${esc(optType)}</td>
|
||||||
<td>${esc(p.pos)}</td>
|
<td>${esc(p.pos)}</td>
|
||||||
|
<td>${optionsExpiryCdHtml(p.exp_time_ms != null ? p.exp_time_ms : p.exp_time)}</td>
|
||||||
<td>${p.idx_px != null ? fmt(p.idx_px, 0) : "—"}</td>
|
<td>${p.idx_px != null ? fmt(p.idx_px, 0) : "—"}</td>
|
||||||
<td>${p.expiry_be_px != null ? fmt(p.expiry_be_px, 0) : "—"}</td>
|
<td>${p.expiry_be_px != null ? fmt(p.expiry_be_px, 0) : "—"}</td>
|
||||||
<td>${p.close_be_px != null ? fmt(p.close_be_px, 0) : "—"}</td>
|
<td>${p.close_be_px != null ? fmt(p.close_be_px, 0) : "—"}</td>
|
||||||
|
|||||||
@@ -118,6 +118,12 @@
|
|||||||
return chips;
|
return chips;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function dashOptionsExpiryCd(expMs) {
|
||||||
|
const ms = expMs != null && expMs !== "" ? String(expMs) : "";
|
||||||
|
if (!ms) return "—";
|
||||||
|
return `<span class="opt-expiry-cd" data-opt-exp-ms="${esc(ms)}">—</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
function renderDashboardOptionsTable(positions) {
|
function renderDashboardOptionsTable(positions) {
|
||||||
const pos = Array.isArray(positions) ? positions : [];
|
const pos = Array.isArray(positions) ? positions : [];
|
||||||
if (!pos.length) return "";
|
if (!pos.length) return "";
|
||||||
@@ -132,6 +138,7 @@
|
|||||||
return `<tr>
|
return `<tr>
|
||||||
<td title="${esc(p.inst_id || "")}">${esc(shortDashInst(p.inst_id))}</td>
|
<td title="${esc(p.inst_id || "")}">${esc(shortDashInst(p.inst_id))}</td>
|
||||||
<td>${esc(optType)}</td>
|
<td>${esc(optType)}</td>
|
||||||
|
<td>${dashOptionsExpiryCd(p.exp_time_ms != null ? p.exp_time_ms : p.exp_time)}</td>
|
||||||
<td>${p.idx_px != null ? fmt(p.idx_px, 0) : "—"}</td>
|
<td>${p.idx_px != null ? fmt(p.idx_px, 0) : "—"}</td>
|
||||||
<td>${p.expiry_be_px != null ? fmt(p.expiry_be_px, 0) : "—"}</td>
|
<td>${p.expiry_be_px != null ? fmt(p.expiry_be_px, 0) : "—"}</td>
|
||||||
<td>${p.close_be_px != null ? fmt(p.close_be_px, 0) : "—"}</td>
|
<td>${p.close_be_px != null ? fmt(p.close_be_px, 0) : "—"}</td>
|
||||||
@@ -144,7 +151,7 @@
|
|||||||
<div class="dash-table-wrap dash-options-table-wrap">
|
<div class="dash-table-wrap dash-options-table-wrap">
|
||||||
<table class="dash-table dash-options-table">
|
<table class="dash-table dash-options-table">
|
||||||
<thead><tr>
|
<thead><tr>
|
||||||
<th>合约</th><th>类型</th><th>指数</th><th>到期平衡</th><th>平掉回本</th><th>浮盈</th>
|
<th>合约</th><th>类型</th><th>到期倒计时</th><th>指数</th><th>到期平衡</th><th>平掉回本</th><th>浮盈</th>
|
||||||
</tr></thead>
|
</tr></thead>
|
||||||
<tbody>${rows}</tbody>
|
<tbody>${rows}</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -303,6 +310,9 @@
|
|||||||
})
|
})
|
||||||
.join("");
|
.join("");
|
||||||
bindDashboardExpand();
|
bindDashboardExpand();
|
||||||
|
if (window.OptionsExpiryCountdown && OptionsExpiryCountdown.ensureTimer) {
|
||||||
|
OptionsExpiryCountdown.ensureTimer();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTrades(trades, accounts) {
|
function renderTrades(trades, accounts) {
|
||||||
|
|||||||
@@ -1146,11 +1146,12 @@
|
|||||||
<script src="/assets/trade_stats_calendar.js?v=3"></script>
|
<script src="/assets/trade_stats_calendar.js?v=3"></script>
|
||||||
<script src="/assets/archive.js?v=20260626-archive-layout"></script>
|
<script src="/assets/archive.js?v=20260626-archive-layout"></script>
|
||||||
<script src="/assets/funds.js?v=20260707-hub-options-funds"></script>
|
<script src="/assets/funds.js?v=20260707-hub-options-funds"></script>
|
||||||
<script src="/assets/dashboard.js?v=20260707-dash-okx-options"></script>
|
<script src="/assets/dashboard.js?v=20260708-options-expiry-cd"></script>
|
||||||
<script src="/assets/strategy.js?v=3"></script>
|
<script src="/assets/strategy.js?v=3"></script>
|
||||||
<script src="/assets/ai_review_render.js?v=3"></script>
|
<script src="/assets/ai_review_render.js?v=3"></script>
|
||||||
<script src="/assets/time_close_ui.js?v=3"></script>
|
<script src="/assets/time_close_ui.js?v=3"></script>
|
||||||
|
<script src="/assets/options_expiry_countdown.js?v=1"></script>
|
||||||
<script src="/assets/backup.js?v=1"></script>
|
<script src="/assets/backup.js?v=1"></script>
|
||||||
<script src="/assets/app.js?v=20260707-monitor-options-split"></script>
|
<script src="/assets/app.js?v=20260708-options-expiry-cd"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -219,6 +219,20 @@ def test_format_position_row_premium_and_inst_parse():
|
|||||||
assert row["opt_type"] == "P"
|
assert row["opt_type"] == "P"
|
||||||
assert row["strike"] == 1700.0
|
assert row["strike"] == 1700.0
|
||||||
assert row["premium_paid"] == 1.24
|
assert row["premium_paid"] == 1.24
|
||||||
|
assert row["exp_time_ms"] is not None
|
||||||
|
assert row["exp_time_ms"] > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_expiry_ms_from_inst_id():
|
||||||
|
from lib.exchange.okx_options_lib import expiry_ms_from_inst_id, normalize_option_exp_ms
|
||||||
|
|
||||||
|
ms = expiry_ms_from_inst_id("ETH-USD_UM-260709-1700-P")
|
||||||
|
assert ms is not None
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
dt = datetime.fromtimestamp(ms / 1000, tz=timezone.utc)
|
||||||
|
assert dt.year == 2026 and dt.month == 7 and dt.day == 9 and dt.hour == 8
|
||||||
|
assert normalize_option_exp_ms(None, "ETH-USD_UM-260709-1700-P") == ms
|
||||||
|
|
||||||
|
|
||||||
def test_format_position_row_breakeven():
|
def test_format_position_row_breakeven():
|
||||||
|
|||||||
Reference in New Issue
Block a user