Add T-shaped options chain view with straddle metrics (phase A).
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3124,6 +3124,7 @@ html[data-theme="light"] .opt-be-dist-down {
|
||||
.opt-uly-btn.active,
|
||||
.opt-type-btn.active,
|
||||
.opt-money-btn.active,
|
||||
.opt-view-btn.active,
|
||||
.opt-pos-tab.active {
|
||||
border-color: #5b8cff;
|
||||
color: #cfe0ff;
|
||||
@@ -3142,6 +3143,98 @@ html[data-theme="light"] .opt-be-dist-down {
|
||||
.opt-strike-row.opt-row-selected td:first-child {
|
||||
box-shadow: inset 3px 0 0 #5b8cff;
|
||||
}
|
||||
.opt-chain-view-group {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
}
|
||||
.opt-type-btn-group {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
}
|
||||
.opt-strike-expand-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-soft, #9aa4b2);
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.opt-strike-expand-label input {
|
||||
margin: 0;
|
||||
}
|
||||
.options-strike-table-wrap--t {
|
||||
max-height: 380px;
|
||||
}
|
||||
.options-strike-table--t thead th {
|
||||
text-align: center;
|
||||
}
|
||||
.options-strike-table--t .opt-t-head-call {
|
||||
text-align: center;
|
||||
color: #8ec5ff;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
.options-strike-table--t .opt-t-head-mid {
|
||||
text-align: center;
|
||||
color: #ffd48a;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
.options-strike-table--t .opt-t-head-put {
|
||||
text-align: center;
|
||||
color: #ff9f9f;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
.options-strike-table--t .opt-t-strike {
|
||||
text-align: center;
|
||||
font-variant-numeric: tabular-nums;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
.options-strike-table--t .opt-t-mid {
|
||||
text-align: center;
|
||||
background: rgba(255, 212, 138, 0.04);
|
||||
}
|
||||
.options-strike-table--t .opt-t-straddle-prem {
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.options-strike-table--t .opt-t-straddle-band {
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
font-size: 0.72rem;
|
||||
color: var(--text-soft, #9aa4b2);
|
||||
}
|
||||
.options-strike-table--t .opt-t-call,
|
||||
.options-strike-table--t .opt-t-put {
|
||||
text-align: center;
|
||||
}
|
||||
.options-strike-table--t .opt-strike-row-atm td {
|
||||
background: rgba(255, 212, 138, 0.08);
|
||||
}
|
||||
.options-strike-table--t .opt-strike-row-atm .opt-t-strike strong {
|
||||
color: #ffd48a;
|
||||
}
|
||||
.options-strike-table--t .opt-strike-row.opt-row-selected td {
|
||||
background: rgba(74, 124, 255, 0.12);
|
||||
}
|
||||
.opt-strike-hint-row td {
|
||||
text-align: center;
|
||||
font-size: 0.72rem;
|
||||
padding: 8px 4px;
|
||||
border-bottom: none;
|
||||
}
|
||||
html[data-theme="light"] .options-strike-table--t .opt-t-head-call {
|
||||
color: #1565c0;
|
||||
}
|
||||
html[data-theme="light"] .options-strike-table--t .opt-t-head-mid {
|
||||
color: #e65100;
|
||||
}
|
||||
html[data-theme="light"] .options-strike-table--t .opt-t-head-put {
|
||||
color: #c62828;
|
||||
}
|
||||
html[data-theme="light"] .options-strike-table--t .opt-strike-row-atm td {
|
||||
background: rgba(255, 152, 0, 0.08);
|
||||
}
|
||||
.opt-order-inline-row td {
|
||||
padding: 14px 16px !important;
|
||||
background: rgba(74, 124, 255, 0.07);
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
underlying: root.dataset.defaultUnderly || "ETH",
|
||||
optType: "C",
|
||||
moneyFilter: "all",
|
||||
chainView: "list",
|
||||
strikeExpandAll: false,
|
||||
chain: panelCache.chain || null,
|
||||
selectedInst: null,
|
||||
orderQuote: null,
|
||||
@@ -84,7 +86,10 @@
|
||||
function placeOrderPanelAfter(instId) {
|
||||
const panel = orderPanel();
|
||||
if (!panel || !instId) return;
|
||||
const row = document.querySelector('#opt-strike-tbody tr.opt-strike-row[data-inst="' + CSS.escape(instId) + '"]');
|
||||
const row =
|
||||
document.querySelector('#opt-strike-tbody tr.opt-strike-row[data-inst="' + CSS.escape(instId) + '"]') ||
|
||||
document.querySelector('#opt-strike-tbody tr.opt-strike-row[data-call-inst="' + CSS.escape(instId) + '"]') ||
|
||||
document.querySelector('#opt-strike-tbody tr.opt-strike-row[data-put-inst="' + CSS.escape(instId) + '"]');
|
||||
if (!row) return;
|
||||
document.querySelectorAll(".opt-strike-row").forEach(function (r) {
|
||||
r.classList.toggle("opt-row-selected", r === row);
|
||||
@@ -100,7 +105,7 @@
|
||||
const tr = document.createElement("tr");
|
||||
tr.className = "opt-order-inline-row";
|
||||
const td = document.createElement("td");
|
||||
td.colSpan = 8;
|
||||
td.colSpan = strikeTableColspan();
|
||||
td.appendChild(panel);
|
||||
tr.appendChild(td);
|
||||
row.after(tr);
|
||||
@@ -135,6 +140,31 @@
|
||||
return url;
|
||||
}
|
||||
|
||||
function strikeTableColspan() {
|
||||
return state.chainView === "t" ? 9 : 8;
|
||||
}
|
||||
|
||||
function syncChainViewUI() {
|
||||
const isT = state.chainView === "t";
|
||||
document.querySelectorAll(".opt-view-btn").forEach(function (b) {
|
||||
b.classList.toggle("active", (b.getAttribute("data-view") || "") === state.chainView);
|
||||
});
|
||||
const typeGroup = document.getElementById("opt-type-btn-group");
|
||||
if (typeGroup) typeGroup.hidden = isT;
|
||||
const expandWrap = document.getElementById("opt-strike-expand-wrap");
|
||||
if (expandWrap) expandWrap.hidden = !isT;
|
||||
const headList = document.getElementById("opt-strike-head-list");
|
||||
const headT = document.getElementById("opt-strike-head-t");
|
||||
const headTCols = document.getElementById("opt-strike-head-t-cols");
|
||||
if (headList) headList.classList.toggle("hidden", isT);
|
||||
if (headT) headT.classList.toggle("hidden", !isT);
|
||||
if (headTCols) headTCols.classList.toggle("hidden", !isT);
|
||||
const wrap = document.getElementById("opt-strike-table-wrap");
|
||||
if (wrap) wrap.classList.toggle("options-strike-table-wrap--t", isT);
|
||||
const table = document.getElementById("opt-strike-table");
|
||||
if (table) table.classList.toggle("options-strike-table--t", isT);
|
||||
}
|
||||
|
||||
function matchesMoneyFilter(moneyness) {
|
||||
const m = (moneyness || "").toLowerCase();
|
||||
if (state.moneyFilter === "all") return true;
|
||||
@@ -149,11 +179,105 @@
|
||||
}
|
||||
|
||||
function countContractsForType(contracts) {
|
||||
if (state.chainView === "t") {
|
||||
return countStraddleStrikes(contracts);
|
||||
}
|
||||
return (contracts || []).filter(function (c) {
|
||||
return c.opt_type === state.optType;
|
||||
}).length;
|
||||
}
|
||||
|
||||
function countStraddleStrikes(contracts) {
|
||||
const strikes = new Set();
|
||||
(contracts || []).forEach(function (c) {
|
||||
if (c.strike != null) strikes.add(String(c.strike));
|
||||
});
|
||||
return strikes.size;
|
||||
}
|
||||
|
||||
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 findAtmStrike(rows, indexPx) {
|
||||
if (!rows.length || indexPx == null || Number.isNaN(Number(indexPx))) return null;
|
||||
let best = rows[0].strike;
|
||||
let bestDist = Math.abs(Number(rows[0].strike) - Number(indexPx));
|
||||
rows.forEach(function (row) {
|
||||
const d = Math.abs(Number(row.strike) - Number(indexPx));
|
||||
if (d < bestDist || (d === bestDist && Number(row.strike) < Number(best))) {
|
||||
bestDist = d;
|
||||
best = row.strike;
|
||||
}
|
||||
});
|
||||
return best;
|
||||
}
|
||||
|
||||
function matchesStrikeRowFilter(strike, indexPx, atmStrike) {
|
||||
if (state.moneyFilter === "all") return true;
|
||||
if (atmStrike != null && Number(strike) === Number(atmStrike)) return true;
|
||||
if (indexPx == null || Number.isNaN(Number(indexPx))) return true;
|
||||
if (state.moneyFilter === "itm") return Number(strike) <= Number(indexPx);
|
||||
if (state.moneyFilter === "otm") return Number(strike) >= Number(indexPx);
|
||||
return true;
|
||||
}
|
||||
|
||||
function filterStraddleRows(rows, indexPx) {
|
||||
const atmStrike = findAtmStrike(rows, indexPx);
|
||||
return rows.filter(function (row) {
|
||||
return matchesStrikeRowFilter(row.strike, indexPx, atmStrike);
|
||||
});
|
||||
}
|
||||
|
||||
function sliceAtmWindow(rows, indexPx) {
|
||||
if (state.strikeExpandAll || !rows.length) return rows;
|
||||
const atmStrike = findAtmStrike(rows, indexPx);
|
||||
const idx = rows.findIndex(function (r) { return Number(r.strike) === Number(atmStrike); });
|
||||
if (idx < 0) return rows.slice(0, Math.min(rows.length, 11));
|
||||
const start = Math.max(0, idx - 5);
|
||||
const end = Math.min(rows.length, idx + 6);
|
||||
return rows.slice(start, end);
|
||||
}
|
||||
|
||||
function straddleAskPerUnit(callAsk, putAsk) {
|
||||
const c = Number(callAsk);
|
||||
const p = Number(putAsk);
|
||||
if (!Number.isFinite(c) || !Number.isFinite(p) || c <= 0 || p <= 0) return null;
|
||||
return Math.round((c + p) * 10000) / 10000;
|
||||
}
|
||||
|
||||
function formatStraddleBand(strike, combinedAsk) {
|
||||
const per = combinedAsk;
|
||||
if (strike == null || per == null) return "—";
|
||||
const k = Number(strike);
|
||||
const d = Number(per);
|
||||
if (!Number.isFinite(k) || !Number.isFinite(d)) return "—";
|
||||
const lo = Math.round((k - d) * 10) / 10;
|
||||
const hi = Math.round((k + d) * 10) / 10;
|
||||
return lo.toFixed(0) + " ~ " + hi.toFixed(0);
|
||||
}
|
||||
|
||||
function formatStraddlePremiumCell(callAsk, putAsk) {
|
||||
const per = straddleAskPerUnit(callAsk, putAsk);
|
||||
if (per == null) return '<span class="muted">不可双买</span>';
|
||||
return fmtUsdc(per) + " USDC";
|
||||
}
|
||||
|
||||
function pickBtnHtml(instId) {
|
||||
if (!instId) return "—";
|
||||
return '<button type="button" class="btn-secondary opt-pick-btn" data-inst="' + instId + '">选择</button>';
|
||||
}
|
||||
|
||||
function syncMoneyFilterButtons() {
|
||||
document.querySelectorAll(".opt-money-btn").forEach(function (b) {
|
||||
b.classList.toggle("active", (b.getAttribute("data-money") || "") === state.moneyFilter);
|
||||
@@ -417,14 +541,38 @@
|
||||
return "";
|
||||
}
|
||||
|
||||
function bindStrikePickButtons(tbody) {
|
||||
tbody.querySelectorAll(".opt-pick-btn").forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
selectContract(btn.getAttribute("data-inst"), btn);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function finishStrikeRender(tbody, prevSelected, matchedSelected) {
|
||||
bindStrikePickButtons(tbody);
|
||||
if (matchedSelected && prevSelected) {
|
||||
selectContract(prevSelected, null, true);
|
||||
} else if (!matchedSelected) {
|
||||
state.selectedInst = null;
|
||||
}
|
||||
}
|
||||
|
||||
function renderStrikes() {
|
||||
syncChainViewUI();
|
||||
if (state.chainView === "t") renderStrikesT();
|
||||
else renderStrikesList();
|
||||
}
|
||||
|
||||
function renderStrikesList() {
|
||||
const tbody = document.getElementById("opt-strike-tbody");
|
||||
const expMs = document.getElementById("opt-exp-select").value;
|
||||
const prevSelected = state.selectedInst;
|
||||
const cols = strikeTableColspan();
|
||||
tbody.innerHTML = "";
|
||||
if (!expMs || !state.chain) {
|
||||
parkOrderPanel();
|
||||
tbody.innerHTML = '<tr><td colspan="8" class="muted">请选择到期日</td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan="' + cols + '" class="muted">请选择到期日</td></tr>';
|
||||
state.selectedInst = null;
|
||||
return;
|
||||
}
|
||||
@@ -441,7 +589,7 @@
|
||||
parkOrderPanel();
|
||||
const label = moneyFilterLabel();
|
||||
const suffix = label ? label : optTypeLabel(state.optType);
|
||||
tbody.innerHTML = '<tr><td colspan="8" class="muted">该到期日暂无' + suffix + "合约</td></tr>";
|
||||
tbody.innerHTML = '<tr><td colspan="' + cols + '" class="muted">该到期日暂无' + suffix + "合约</td></tr>";
|
||||
state.selectedInst = null;
|
||||
return;
|
||||
}
|
||||
@@ -462,21 +610,86 @@
|
||||
"<td>" + (c.expiry_be_px != null ? fmt(c.expiry_be_px, 0) : "—") + "</td>" +
|
||||
'<td class="' + distBeClass(c.dist_expiry_be) + '">' + fmtDist(c.dist_expiry_be) + "</td>" +
|
||||
'<td class="opt-row-actions">' +
|
||||
'<button type="button" class="btn-secondary opt-pick-btn" data-inst="' + c.inst_id + '">选择</button>' +
|
||||
pickBtnHtml(c.inst_id) +
|
||||
"</td>";
|
||||
tbody.appendChild(tr);
|
||||
if (c.inst_id === prevSelected) matchedSelected = true;
|
||||
});
|
||||
tbody.querySelectorAll(".opt-pick-btn").forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
selectContract(btn.getAttribute("data-inst"), btn);
|
||||
});
|
||||
});
|
||||
if (matchedSelected && prevSelected) {
|
||||
selectContract(prevSelected, null, true);
|
||||
} else {
|
||||
finishStrikeRender(tbody, prevSelected, matchedSelected);
|
||||
}
|
||||
|
||||
function renderStrikesT() {
|
||||
const tbody = document.getElementById("opt-strike-tbody");
|
||||
const expMs = document.getElementById("opt-exp-select").value;
|
||||
const prevSelected = state.selectedInst;
|
||||
const cols = strikeTableColspan();
|
||||
const indexPx = state.chain && state.chain.index_px;
|
||||
tbody.innerHTML = "";
|
||||
if (!expMs || !state.chain) {
|
||||
parkOrderPanel();
|
||||
tbody.innerHTML = '<tr><td colspan="' + cols + '" class="muted">请选择到期日</td></tr>';
|
||||
state.selectedInst = null;
|
||||
return;
|
||||
}
|
||||
const exp = (state.chain.expiries || []).find(function (e) {
|
||||
return String(e.exp_time) === String(expMs);
|
||||
});
|
||||
if (!exp) {
|
||||
parkOrderPanel();
|
||||
state.selectedInst = null;
|
||||
return;
|
||||
}
|
||||
let rows = filterStraddleRows(buildStraddleRows(exp.contracts), indexPx);
|
||||
rows = sliceAtmWindow(rows, indexPx);
|
||||
if (!rows.length) {
|
||||
parkOrderPanel();
|
||||
const label = moneyFilterLabel();
|
||||
const suffix = label ? label + "区" : "匹配";
|
||||
tbody.innerHTML = '<tr><td colspan="' + cols + '" class="muted">该到期日暂无' + suffix + "行权价</td></tr>";
|
||||
state.selectedInst = null;
|
||||
return;
|
||||
}
|
||||
const atmStrike = findAtmStrike(rows, indexPx);
|
||||
const visibleInsts = [];
|
||||
rows.forEach(function (row) {
|
||||
if (row.call && row.call.inst_id) visibleInsts.push(row.call.inst_id);
|
||||
if (row.put && row.put.inst_id) visibleInsts.push(row.put.inst_id);
|
||||
});
|
||||
const stillVisible = !!prevSelected && visibleInsts.indexOf(prevSelected) >= 0;
|
||||
if (!stillVisible) parkOrderPanel();
|
||||
let matchedSelected = false;
|
||||
rows.forEach(function (row) {
|
||||
const call = row.call;
|
||||
const put = row.put;
|
||||
const combined = straddleAskPerUnit(call && call.ask, put && put.ask);
|
||||
const tr = document.createElement("tr");
|
||||
tr.className = "opt-strike-row opt-strike-row-t";
|
||||
tr.setAttribute("data-strike", String(row.strike));
|
||||
if (Number(row.strike) === Number(atmStrike)) tr.classList.add("opt-strike-row-atm");
|
||||
if (call && call.inst_id) tr.setAttribute("data-call-inst", call.inst_id);
|
||||
if (put && put.inst_id) tr.setAttribute("data-put-inst", put.inst_id);
|
||||
tr.innerHTML =
|
||||
'<td class="opt-t-call opt-px-sz">' + (call ? fmtPxSz(call.ask, call.ask_sz, call.ask_estimated) : "—") + "</td>" +
|
||||
'<td class="opt-t-call">' + (call ? moneynessBadge(call) : "—") + "</td>" +
|
||||
'<td class="opt-t-call opt-row-actions">' + pickBtnHtml(call && call.inst_id) + "</td>" +
|
||||
'<td class="opt-t-mid opt-t-strike"><strong>' + row.strike + "</strong></td>" +
|
||||
'<td class="opt-t-mid opt-t-straddle-prem">' + formatStraddlePremiumCell(call && call.ask, put && put.ask) + "</td>" +
|
||||
'<td class="opt-t-mid opt-t-straddle-band">' + formatStraddleBand(row.strike, combined) + "</td>" +
|
||||
'<td class="opt-t-put">' + (put ? moneynessBadge(put) : "—") + "</td>" +
|
||||
'<td class="opt-t-put opt-px-sz">' + (put ? fmtPxSz(put.ask, put.ask_sz, put.ask_estimated) : "—") + "</td>" +
|
||||
'<td class="opt-t-put opt-row-actions">' + pickBtnHtml(put && put.inst_id) + "</td>";
|
||||
tbody.appendChild(tr);
|
||||
if (prevSelected && ((call && call.inst_id === prevSelected) || (put && put.inst_id === prevSelected))) {
|
||||
matchedSelected = true;
|
||||
}
|
||||
});
|
||||
if (!state.strikeExpandAll && rows.length >= 1) {
|
||||
const hint = document.createElement("tr");
|
||||
hint.className = "opt-strike-hint-row";
|
||||
hint.innerHTML = '<td colspan="' + cols + '" class="muted opt-strike-hint">默认显示 ATM ±5 档 · 勾选「展开全部」查看该到期全部行权价</td>';
|
||||
tbody.appendChild(hint);
|
||||
}
|
||||
finishStrikeRender(tbody, prevSelected, matchedSelected);
|
||||
}
|
||||
|
||||
function fillOrderPanel(d) {
|
||||
@@ -547,6 +760,9 @@
|
||||
panelCache.optType = state.optType;
|
||||
state.selectedInst = null;
|
||||
resetMoneyFilterToAll();
|
||||
state.strikeExpandAll = false;
|
||||
const expandCb = document.getElementById("opt-strike-expand-all");
|
||||
if (expandCb) expandCb.checked = false;
|
||||
parkOrderPanel();
|
||||
updateUnderlyingLabel();
|
||||
renderExpiries();
|
||||
@@ -1050,12 +1266,16 @@
|
||||
|
||||
function onExpiryChange() {
|
||||
resetMoneyFilterToAll();
|
||||
state.strikeExpandAll = false;
|
||||
const expandCb = document.getElementById("opt-strike-expand-all");
|
||||
if (expandCb) expandCb.checked = false;
|
||||
renderStrikes();
|
||||
}
|
||||
|
||||
function bootOptionsPanel() {
|
||||
updateSizeInputs();
|
||||
syncMoneyFilterButtons();
|
||||
syncChainViewUI();
|
||||
updateUnderlyingLabel();
|
||||
const hasCache =
|
||||
panelCache.chain &&
|
||||
@@ -1083,6 +1303,29 @@
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll(".opt-view-btn").forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
const view = btn.getAttribute("data-view") || "list";
|
||||
if (view === state.chainView) return;
|
||||
state.chainView = view;
|
||||
if (view === "t") {
|
||||
state.strikeExpandAll = false;
|
||||
const expandCb = document.getElementById("opt-strike-expand-all");
|
||||
if (expandCb) expandCb.checked = false;
|
||||
}
|
||||
syncChainViewUI();
|
||||
renderStrikes();
|
||||
});
|
||||
});
|
||||
|
||||
const expandAllCb = document.getElementById("opt-strike-expand-all");
|
||||
if (expandAllCb) {
|
||||
expandAllCb.addEventListener("change", function () {
|
||||
state.strikeExpandAll = !!expandAllCb.checked;
|
||||
renderStrikes();
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll(".opt-type-btn").forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
document.querySelectorAll(".opt-type-btn").forEach(function (b) { b.classList.remove("active"); });
|
||||
|
||||
@@ -358,3 +358,49 @@ def equivalent_contract_leverage(
|
||||
if eth_amount <= 0 or total_premium <= 0:
|
||||
return None
|
||||
return round(float(index_px) * float(eth_amount) / float(total_premium), 1)
|
||||
|
||||
|
||||
def straddle_ask_per_unit(
|
||||
call_ask: float | None,
|
||||
put_ask: float | None,
|
||||
) -> float | None:
|
||||
"""跨式双买:每 1 标的币的卖一报价之和."""
|
||||
if call_ask is None or put_ask is None:
|
||||
return None
|
||||
if float(call_ask) <= 0 or float(put_ask) <= 0:
|
||||
return None
|
||||
return round(float(call_ask) + float(put_ask), 4)
|
||||
|
||||
|
||||
def straddle_premium_total(
|
||||
call_ask: float | None,
|
||||
put_ask: float | None,
|
||||
eth_amount: float | None,
|
||||
) -> float | None:
|
||||
"""跨式双买权利金总额(USDC)."""
|
||||
per = straddle_ask_per_unit(call_ask, put_ask)
|
||||
if per is None or eth_amount is None or float(eth_amount) <= 0:
|
||||
return None
|
||||
return round(per * float(eth_amount), 2)
|
||||
|
||||
|
||||
def straddle_breakeven_band(
|
||||
strike: float | None,
|
||||
combined_ask_per_unit: float | None,
|
||||
) -> tuple[float | None, float | None]:
|
||||
"""跨式到期平衡带:下平衡 ~ 上平衡(按双卖一报价和)."""
|
||||
if strike is None or combined_ask_per_unit is None:
|
||||
return None, None
|
||||
k = float(strike)
|
||||
d = float(combined_ask_per_unit)
|
||||
return round(k - d, 2), round(k + d, 2)
|
||||
|
||||
|
||||
def format_straddle_band(
|
||||
strike: float | None,
|
||||
combined_ask_per_unit: float | None,
|
||||
) -> str:
|
||||
lo, hi = straddle_breakeven_band(strike, combined_ask_per_unit)
|
||||
if lo is None or hi is None:
|
||||
return ""
|
||||
return f"{lo:.0f} ~ {hi:.0f}"
|
||||
|
||||
@@ -7,23 +7,32 @@
|
||||
<div class="options-dual-grid">
|
||||
<div class="card options-order-card">
|
||||
<h2>期权下单</h2>
|
||||
<p class="muted options-hint">报价单位为每 1 ETH/BTC;1 张 = 0.01.卖一/买一列为 <strong>价格/张数</strong>;卖一无挂单时以标记价估算并标 <strong>~</strong>.链展示近 <span id="opt-chain-dte">14</span> 日到期,标注实值/虚值;<strong>到期平衡</strong>按卖一预估(无卖一按标记价).资金划转与 USDT/USDC 兑换见「系统设置 → 期权设置」.</p>
|
||||
<p class="muted options-hint">报价单位为每 1 ETH/BTC;1 张 = 0.01.<strong>列表</strong>含卖一/买一;<strong>T 型</strong>仅卖一(买方开仓),中间为跨式双买测算.卖一无挂单时以标记价估算并标 <strong>~</strong>.链展示近 <span id="opt-chain-dte">14</span> 日到期.<strong>T 型</strong>默认 ATM ±5 档,可展开全部.</p>
|
||||
<div class="form-row options-chain-toolbar">
|
||||
<button type="button" class="btn-secondary opt-uly-btn active" data-uly="ETH">ETH</button>
|
||||
<button type="button" class="btn-secondary opt-uly-btn" data-uly="BTC">BTC</button>
|
||||
<select id="opt-exp-select"><option value="">选择到期日</option></select>
|
||||
<button type="button" class="btn-secondary opt-type-btn active" data-type="C">看涨 Call</button>
|
||||
<button type="button" class="btn-secondary opt-type-btn" data-type="P">看跌 Put</button>
|
||||
<span class="opt-chain-view-group">
|
||||
<button type="button" class="btn-secondary opt-view-btn active" data-view="list">列表</button>
|
||||
<button type="button" class="btn-secondary opt-view-btn" data-view="t">T 型</button>
|
||||
</span>
|
||||
<span id="opt-type-btn-group" class="opt-type-btn-group">
|
||||
<button type="button" class="btn-secondary opt-type-btn active" data-type="C">看涨 Call</button>
|
||||
<button type="button" class="btn-secondary opt-type-btn" data-type="P">看跌 Put</button>
|
||||
</span>
|
||||
<button type="button" class="btn-secondary opt-money-btn active" data-money="all">全部</button>
|
||||
<button type="button" class="btn-secondary opt-money-btn" data-money="itm">实值</button>
|
||||
<button type="button" class="btn-secondary opt-money-btn" data-money="otm">虚值</button>
|
||||
<label id="opt-strike-expand-wrap" class="opt-strike-expand-label" hidden>
|
||||
<input type="checkbox" id="opt-strike-expand-all"> 展开全部
|
||||
</label>
|
||||
<button type="button" class="btn-secondary" id="opt-load-chain">刷新链</button>
|
||||
</div>
|
||||
<div id="opt-index-line" class="muted"></div>
|
||||
<div class="options-strike-table-wrap">
|
||||
<div class="options-strike-table-wrap" id="opt-strike-table-wrap">
|
||||
<table class="options-strike-table" id="opt-strike-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<tr id="opt-strike-head-list">
|
||||
<th>行权价</th>
|
||||
<th>类型</th>
|
||||
<th>合约</th>
|
||||
@@ -33,6 +42,22 @@
|
||||
<th>距平衡</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
<tr id="opt-strike-head-t" class="hidden">
|
||||
<th colspan="3" class="opt-t-head-call">Call</th>
|
||||
<th colspan="3" class="opt-t-head-mid">跨式</th>
|
||||
<th colspan="3" class="opt-t-head-put">Put</th>
|
||||
</tr>
|
||||
<tr id="opt-strike-head-t-cols" class="hidden">
|
||||
<th>卖一/张</th>
|
||||
<th>类型</th>
|
||||
<th>操作</th>
|
||||
<th>行权价</th>
|
||||
<th title="Call卖一+Put卖一(每1币)">双买/币</th>
|
||||
<th title="到期测算平衡带">平衡带</th>
|
||||
<th>类型</th>
|
||||
<th>卖一/张</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="opt-strike-tbody">
|
||||
<tr><td colspan="8" class="muted">请选择到期日</td></tr>
|
||||
@@ -215,4 +240,4 @@
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/options_expiry_countdown.js?v=1"></script>
|
||||
<script src="/static/options_panel.js?v=27"></script>
|
||||
<script src="/static/options_panel.js?v=28"></script>
|
||||
|
||||
@@ -75,6 +75,23 @@ def test_equivalent_contract_leverage():
|
||||
assert lev == 144.9
|
||||
|
||||
|
||||
def test_straddle_pricing():
|
||||
from lib.options.options_pricing_lib import (
|
||||
format_straddle_band,
|
||||
straddle_ask_per_unit,
|
||||
straddle_breakeven_band,
|
||||
straddle_premium_total,
|
||||
)
|
||||
|
||||
assert straddle_ask_per_unit(0.148, 16.2) == 16.348
|
||||
assert straddle_premium_total(0.148, 16.2, 1.0) == 16.35
|
||||
lo, hi = straddle_breakeven_band(1800, 16.348)
|
||||
assert lo == 1783.65
|
||||
assert hi == 1816.35
|
||||
assert format_straddle_band(1800, 16.348) == "1784 ~ 1816"
|
||||
assert straddle_ask_per_unit(0.148, None) is None
|
||||
|
||||
|
||||
def test_estimate_expiry_value_and_profit_at_index():
|
||||
from lib.options.options_pricing_lib import (
|
||||
estimate_expiry_profit_at_index,
|
||||
|
||||
Reference in New Issue
Block a user