Add expiry profit estimate and remove chain quick-buy button.
Let users enter a target index in the order panel for estimated expiry P&L, and remove the redundant buy button from the options chain row. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2658,6 +2658,40 @@ html[data-theme="light"] .opt-be-dist-down {
|
|||||||
.options-page-wrap .options-order-grid .v {
|
.options-page-wrap .options-order-grid .v {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
.options-estimate-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px 12px;
|
||||||
|
margin: 8px 0 10px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border: 1px dashed rgba(255, 255, 255, 0.08);
|
||||||
|
font-size: 0.74rem;
|
||||||
|
}
|
||||||
|
.options-estimate-row .opt-est-label {
|
||||||
|
color: #8892b0;
|
||||||
|
}
|
||||||
|
.options-estimate-row .opt-target-idx {
|
||||||
|
width: 120px;
|
||||||
|
font-size: 0.74rem;
|
||||||
|
padding: 3px 6px;
|
||||||
|
}
|
||||||
|
.options-estimate-row .k {
|
||||||
|
color: #8892b0;
|
||||||
|
}
|
||||||
|
.options-estimate-row .v {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.options-estimate-row .opt-est-note {
|
||||||
|
font-size: 0.66rem;
|
||||||
|
}
|
||||||
|
html[data-theme="light"] .options-estimate-row {
|
||||||
|
background: rgba(0, 0, 0, 0.02);
|
||||||
|
border-color: rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
.options-hint {
|
.options-hint {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
optType: "C",
|
optType: "C",
|
||||||
chain: panelCache.chain || null,
|
chain: panelCache.chain || null,
|
||||||
selectedInst: null,
|
selectedInst: null,
|
||||||
|
orderQuote: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
function fmt(v, d) {
|
function fmt(v, d) {
|
||||||
@@ -72,7 +73,7 @@
|
|||||||
const tr = document.createElement("tr");
|
const tr = document.createElement("tr");
|
||||||
tr.className = "opt-order-inline-row";
|
tr.className = "opt-order-inline-row";
|
||||||
const td = document.createElement("td");
|
const td = document.createElement("td");
|
||||||
td.colSpan = 6;
|
td.colSpan = 8;
|
||||||
td.appendChild(panel);
|
td.appendChild(panel);
|
||||||
tr.appendChild(td);
|
tr.appendChild(td);
|
||||||
row.after(tr);
|
row.after(tr);
|
||||||
@@ -158,6 +159,60 @@
|
|||||||
return price + "/" + size;
|
return price + "/" + size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pnlCls(v) {
|
||||||
|
if (v === null || v === undefined || Number.isNaN(Number(v))) return "";
|
||||||
|
const n = Number(v);
|
||||||
|
if (n > 0) return "pos-pnl-profit";
|
||||||
|
if (n < 0) return "pos-pnl-loss";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function estimateExpiryProfit(optType, strike, targetIdx, entryPx, ethAmount) {
|
||||||
|
if (strike == null || targetIdx == null || entryPx == null || ethAmount == null) return null;
|
||||||
|
const amt = Number(ethAmount);
|
||||||
|
const entry = Number(entryPx);
|
||||||
|
const tgt = Number(targetIdx);
|
||||||
|
const k = Number(strike);
|
||||||
|
if (!Number.isFinite(amt) || !Number.isFinite(entry) || !Number.isFinite(tgt) || !Number.isFinite(k) || amt <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const o = (optType || "").toUpperCase();
|
||||||
|
let intrinsic = 0;
|
||||||
|
if (o === "C") intrinsic = Math.max(0, tgt - k);
|
||||||
|
else if (o === "P") intrinsic = Math.max(0, k - tgt);
|
||||||
|
else return null;
|
||||||
|
return Math.round((intrinsic - entry) * amt * 10000) / 10000;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateEstimatedProfit() {
|
||||||
|
const el = document.getElementById("opt-est-profit");
|
||||||
|
const targetEl = document.getElementById("opt-target-idx");
|
||||||
|
if (!el || !targetEl) return;
|
||||||
|
const q = state.orderQuote;
|
||||||
|
if (!q || !q.ok) {
|
||||||
|
el.textContent = "—";
|
||||||
|
el.className = "v";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const targetRaw = targetEl.value;
|
||||||
|
if (targetRaw === "" || targetRaw == null) {
|
||||||
|
el.textContent = "—";
|
||||||
|
el.className = "v";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const sz = q.sizing || {};
|
||||||
|
const ethAmount = sz.eth_amount;
|
||||||
|
const profit = estimateExpiryProfit(q.opt_type, q.strike, Number(targetRaw), q.ask, ethAmount);
|
||||||
|
if (profit == null || Number.isNaN(profit)) {
|
||||||
|
el.textContent = "—";
|
||||||
|
el.className = "v";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const sign = profit > 0 ? "+" : "";
|
||||||
|
el.textContent = sign + profit.toFixed(4) + " USDC";
|
||||||
|
el.className = "v " + pnlCls(profit);
|
||||||
|
}
|
||||||
|
|
||||||
function fmtDist(v) {
|
function fmtDist(v) {
|
||||||
if (v === null || v === undefined || Number.isNaN(Number(v))) return "—";
|
if (v === null || v === undefined || Number.isNaN(Number(v))) return "—";
|
||||||
const n = Number(v);
|
const n = Number(v);
|
||||||
@@ -211,8 +266,7 @@
|
|||||||
"<td>" + (c.expiry_be_px != null ? fmt(c.expiry_be_px, 0) : "—") + "</td>" +
|
"<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="' + distBeClass(c.dist_expiry_be) + '">' + fmtDist(c.dist_expiry_be) + "</td>" +
|
||||||
'<td class="opt-row-actions">' +
|
'<td class="opt-row-actions">' +
|
||||||
'<button type="button" class="btn-secondary opt-pick-btn" data-inst="' + c.inst_id + '">选择</button> ' +
|
'<button type="button" class="btn-secondary opt-pick-btn" data-inst="' + c.inst_id + '">选择</button>' +
|
||||||
'<button type="button" class="btn-primary opt-buy-btn" data-inst="' + c.inst_id + '">买入</button>' +
|
|
||||||
"</td>";
|
"</td>";
|
||||||
tbody.appendChild(tr);
|
tbody.appendChild(tr);
|
||||||
if (c.inst_id === prevSelected) stillVisible = true;
|
if (c.inst_id === prevSelected) stillVisible = true;
|
||||||
@@ -222,12 +276,6 @@
|
|||||||
selectContract(btn.getAttribute("data-inst"), btn);
|
selectContract(btn.getAttribute("data-inst"), btn);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
tbody.querySelectorAll(".opt-buy-btn").forEach(function (btn) {
|
|
||||||
btn.addEventListener("click", async function () {
|
|
||||||
await selectContract(btn.getAttribute("data-inst"), null);
|
|
||||||
await openPosition();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
if (stillVisible && prevSelected) {
|
if (stillVisible && prevSelected) {
|
||||||
selectContract(prevSelected, null, true);
|
selectContract(prevSelected, null, true);
|
||||||
} else {
|
} else {
|
||||||
@@ -236,6 +284,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function fillOrderPanel(d) {
|
function fillOrderPanel(d) {
|
||||||
|
state.orderQuote = d && d.ok ? d : null;
|
||||||
const sz = d.sizing || {};
|
const sz = d.sizing || {};
|
||||||
document.getElementById("opt-order-inst").textContent = d.inst_id || state.selectedInst || "";
|
document.getElementById("opt-order-inst").textContent = d.inst_id || state.selectedInst || "";
|
||||||
document.getElementById("opt-order-ask").textContent = fmtPxSz(d.ask, d.ask_sz);
|
document.getElementById("opt-order-ask").textContent = fmtPxSz(d.ask, d.ask_sz);
|
||||||
@@ -264,6 +313,7 @@
|
|||||||
msgEl.textContent = "";
|
msgEl.textContent = "";
|
||||||
msgEl.classList.remove("opt-error");
|
msgEl.classList.remove("opt-error");
|
||||||
}
|
}
|
||||||
|
updateEstimatedProfit();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function selectContract(instId, pickBtn, silent) {
|
async function selectContract(instId, pickBtn, silent) {
|
||||||
@@ -553,12 +603,19 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
["opt-sheets-amount", "opt-eth-amount"].forEach(function (id) {
|
["opt-sheets-amount", "opt-eth-amount", "opt-target-idx"].forEach(function (id) {
|
||||||
const el = document.getElementById(id);
|
const el = document.getElementById(id);
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
el.addEventListener("change", function () {
|
el.addEventListener("change", function () {
|
||||||
|
if (id === "opt-target-idx") {
|
||||||
|
updateEstimatedProfit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (state.selectedInst) selectContract(state.selectedInst, null, true);
|
if (state.selectedInst) selectContract(state.selectedInst, null, true);
|
||||||
});
|
});
|
||||||
|
if (id === "opt-target-idx") {
|
||||||
|
el.addEventListener("input", updateEstimatedProfit);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
bootOptionsPanel();
|
bootOptionsPanel();
|
||||||
|
|||||||
@@ -234,3 +234,26 @@ def format_options_breakeven_line(
|
|||||||
if idx_px is not None and parts:
|
if idx_px is not None and parts:
|
||||||
return " ".join(parts) + f"(指数{idx_px:.0f})"
|
return " ".join(parts) + f"(指数{idx_px:.0f})"
|
||||||
return " ".join(parts)
|
return " ".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def estimate_expiry_profit_at_index(
|
||||||
|
*,
|
||||||
|
opt_type: str,
|
||||||
|
strike: float | None,
|
||||||
|
target_idx: float | None,
|
||||||
|
entry_px: float | None,
|
||||||
|
eth_amount: float | None,
|
||||||
|
) -> float | None:
|
||||||
|
"""到期测算:目标指数价下按内在价值减权利金(每 1 币报价 × 币量)。"""
|
||||||
|
if strike is None or target_idx is None or entry_px is None or eth_amount is None:
|
||||||
|
return None
|
||||||
|
if eth_amount <= 0:
|
||||||
|
return None
|
||||||
|
o = (opt_type or "").upper()
|
||||||
|
if o == "C":
|
||||||
|
intrinsic = max(0.0, float(target_idx) - float(strike))
|
||||||
|
elif o == "P":
|
||||||
|
intrinsic = max(0.0, float(strike) - float(target_idx))
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
return round((intrinsic - float(entry_px)) * float(eth_amount), 4)
|
||||||
|
|||||||
@@ -49,6 +49,13 @@
|
|||||||
<div><span class="k">到期平衡</span><span id="opt-order-expiry-be" class="v">—</span></div>
|
<div><span class="k">到期平衡</span><span id="opt-order-expiry-be" class="v">—</span></div>
|
||||||
<div><span class="k">距平衡</span><span id="opt-order-dist-be" class="v">—</span></div>
|
<div><span class="k">距平衡</span><span id="opt-order-dist-be" class="v">—</span></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="options-estimate-row">
|
||||||
|
<label class="opt-est-label" for="opt-target-idx">目标位(指数)</label>
|
||||||
|
<input type="number" id="opt-target-idx" class="opt-target-idx" step="0.1" min="0" placeholder="到期时指数价">
|
||||||
|
<span class="k">预计盈利</span>
|
||||||
|
<span id="opt-est-profit" class="v">—</span>
|
||||||
|
<span class="muted opt-est-note">到期测算,仅供参考</span>
|
||||||
|
</div>
|
||||||
<div class="form-row options-order-mode-row">
|
<div class="form-row options-order-mode-row">
|
||||||
<label><input type="radio" name="opt-size-mode" value="sheets" checked> 指定张数</label>
|
<label><input type="radio" name="opt-size-mode" value="sheets" checked> 指定张数</label>
|
||||||
<input type="number" id="opt-sheets-amount" min="1" step="1" value="1" placeholder="张数">
|
<input type="number" id="opt-sheets-amount" min="1" step="1" value="1" placeholder="张数">
|
||||||
|
|||||||
@@ -67,6 +67,21 @@ def test_option_moneyness():
|
|||||||
assert option_moneyness_label("otm") == "虚值"
|
assert option_moneyness_label("otm") == "虚值"
|
||||||
|
|
||||||
|
|
||||||
|
def test_estimate_expiry_profit_at_index():
|
||||||
|
from lib.options.options_pricing_lib import estimate_expiry_profit_at_index
|
||||||
|
|
||||||
|
# Call 1780, ask 12.2, 0.01 ETH, target 1793 -> (13-12.2)*0.01 = 0.008
|
||||||
|
p = estimate_expiry_profit_at_index(
|
||||||
|
opt_type="C", strike=1780, target_idx=1793, entry_px=12.2, eth_amount=0.01
|
||||||
|
)
|
||||||
|
assert p == 0.008
|
||||||
|
# OTM call loses premium
|
||||||
|
p2 = estimate_expiry_profit_at_index(
|
||||||
|
opt_type="C", strike=1780, target_idx=1770, entry_px=12.2, eth_amount=0.01
|
||||||
|
)
|
||||||
|
assert p2 == round(-12.2 * 0.01, 4)
|
||||||
|
|
||||||
|
|
||||||
def test_format_quote_liquidity():
|
def test_format_quote_liquidity():
|
||||||
from lib.options.options_pricing_lib import format_quote_liquidity
|
from lib.options.options_pricing_lib import format_quote_liquidity
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user