Add hub perpetual-options hedge calculator tab and sizing formula.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-28 12:37:51 +08:00
parent f360242188
commit 4bcf88b5cb
6 changed files with 496 additions and 5 deletions
+32
View File
@@ -1269,6 +1269,18 @@ class RollCalculatorBody(BaseModel):
base: str = "ETH"
class PerpOptionsCalculatorBody(BaseModel):
base: str = "ETH"
spot: float = Field(gt=0)
capital_usdt: float = Field(gt=0)
target_profit_u: float = Field(ge=0)
move_mode: str = "points"
move_value: float = Field(gt=0)
perp_leverage: float = Field(gt=0)
option_leverage: float = Field(gt=0)
ct_mult: float = Field(default=0.01, gt=0)
class CompareOptionLegBody(BaseModel):
opt_type: str = "C"
strike: float | None = None
@@ -1350,6 +1362,26 @@ def api_calculator_roll(body: RollCalculatorBody):
return {"ok": True, "data": data}
@app.post("/api/calculator/perp-options")
def api_calculator_perp_options(body: PerpOptionsCalculatorBody):
from lib.hub.hub_perp_options_calc_lib import calc_perp_options_hedge
data, err = calc_perp_options_hedge(
base=body.base,
spot=body.spot,
capital_usdt=body.capital_usdt,
target_profit_u=body.target_profit_u,
move_mode=body.move_mode,
move_value=body.move_value,
perp_leverage=body.perp_leverage,
option_leverage=body.option_leverage,
ct_mult=body.ct_mult,
)
if err:
return JSONResponse({"ok": False, "msg": err}, status_code=400)
return {"ok": True, "data": data}
@app.post("/api/compare/calc")
def api_compare_calc(body: CompareBody):
from lib.hub.hub_compare_lib import run_compare
+29 -2
View File
@@ -4708,7 +4708,11 @@ body.login-page {
}
body.hub-phone .calc-layout[data-calc-tab="trend"] [data-calc-pane="roll"],
body.hub-phone .calc-layout[data-calc-tab="roll"] [data-calc-pane="trend"] {
body.hub-phone .calc-layout[data-calc-tab="trend"] [data-calc-pane="po"],
body.hub-phone .calc-layout[data-calc-tab="roll"] [data-calc-pane="trend"],
body.hub-phone .calc-layout[data-calc-tab="roll"] [data-calc-pane="po"],
body.hub-phone .calc-layout[data-calc-tab="po"] [data-calc-pane="trend"],
body.hub-phone .calc-layout[data-calc-tab="po"] [data-calc-pane="roll"] {
display: none;
}
@@ -9776,10 +9780,33 @@ body:not(.hub-phone) #page-calculator .calc-layout {
}
body:not(.hub-phone) #page-calculator .calc-layout[data-calc-tab="trend"] [data-calc-pane="roll"],
body:not(.hub-phone) #page-calculator .calc-layout[data-calc-tab="roll"] [data-calc-pane="trend"] {
body:not(.hub-phone) #page-calculator .calc-layout[data-calc-tab="trend"] [data-calc-pane="po"],
body:not(.hub-phone) #page-calculator .calc-layout[data-calc-tab="roll"] [data-calc-pane="trend"],
body:not(.hub-phone) #page-calculator .calc-layout[data-calc-tab="roll"] [data-calc-pane="po"],
body:not(.hub-phone) #page-calculator .calc-layout[data-calc-tab="po"] [data-calc-pane="trend"],
body:not(.hub-phone) #page-calculator .calc-layout[data-calc-tab="po"] [data-calc-pane="roll"] {
display: none;
}
#page-calculator .calc-po-cases {
display: grid;
gap: 12px;
margin-top: 12px;
}
#page-calculator .calc-po-case {
padding: 10px 12px;
border: 1px solid var(--border-soft, rgba(255, 255, 255, 0.08));
border-radius: 10px;
background: color-mix(in srgb, var(--panel, #12161f) 88%, transparent);
}
#page-calculator .calc-po-case h4 {
margin: 0 0 8px;
font-size: 13px;
font-weight: 600;
}
body:not(.hub-phone) #page-calculator .calc-card {
padding: 0;
background: transparent;
+135 -1
View File
@@ -572,8 +572,135 @@
}
}
function renderPerpOptionsResult(data) {
const box = $("calc-po-result");
if (!box) return;
const a = data.case_a || {};
const b = data.case_b || {};
const capitalHint = data.capital_ok
? "资金充足(参考)"
: "保证金高于交易资金(仅提示)";
box.classList.remove("hidden");
box.innerHTML =
'<div class="calc-summary">' +
"<div><span>标的</span><strong>" +
esc(data.base || "—") +
" · 永续 " +
fmtTrim(data.perp_coins, 4) +
" 币</strong></div>" +
"<div><span>单币权利金</span><strong>" +
fmt(data.prem_per_coin, 4) +
"U</strong></div>" +
"<div><span>永续毛收益</span><strong class=\"" +
pnlClass(data.perp_gross_u) +
'">' +
fmtU(data.perp_gross_u) +
"</strong></div>" +
"<div><span>永续手续费</span><strong>" +
fmt(data.perp_fee_u, 4) +
"U</strong></div>" +
"<div><span>权利金预算</span><strong>" +
fmt(data.premium_budget_u, 4) +
"U</strong></div>" +
"<div><span>期权开仓</span><strong>" +
fmtTrim(data.opt_coins, 6) +
" 币 / " +
fmtTrim(data.opt_sheets, 4) +
" 张</strong></div>" +
"<div><span>永续保证金</span><strong>" +
fmt(data.perp_margin_u, 2) +
"U</strong></div>" +
"<div><span>开仓参考</span><strong>" +
esc(capitalHint) +
"</strong></div>" +
"</div>" +
'<div class="calc-po-cases">' +
'<section class="calc-po-case">' +
"<h4>情景 A · 永续方向对</h4>" +
'<div class="calc-summary">' +
"<div><span>永续盈亏</span><strong class=\"" +
pnlClass(a.perp_pnl_u) +
'">' +
fmtU(a.perp_pnl_u) +
"</strong></div>" +
"<div><span>权利金(全亏)</span><strong>" +
fmt(a.premium_u, 4) +
"U</strong></div>" +
"<div><span>手续费</span><strong>" +
fmt(a.fee_u, 4) +
"U</strong></div>" +
"<div><span>净利</span><strong class=\"" +
pnlClass(a.net_u) +
'">' +
fmtU(a.net_u) +
"</strong></div>" +
"</div></section>" +
'<section class="calc-po-case">' +
"<h4>情景 B · 期权方向对</h4>" +
'<div class="calc-summary">' +
"<div><span>期权内在</span><strong>" +
fmtU(b.opt_intrinsic_u) +
"</strong></div>" +
"<div><span>权利金</span><strong>" +
fmt(b.premium_u, 4) +
"U</strong></div>" +
"<div><span>期权净利</span><strong class=\"" +
pnlClass(b.opt_net_u) +
'">' +
fmtU(b.opt_net_u) +
"</strong></div>" +
"<div><span>永续盈亏</span><strong class=\"" +
pnlClass(b.perp_pnl_u) +
'">' +
fmtU(b.perp_pnl_u) +
"</strong></div>" +
"<div><span>组合净利</span><strong class=\"" +
pnlClass(b.portfolio_net_u) +
'">' +
fmtU(b.portfolio_net_u) +
"</strong></div>" +
"</div></section></div>";
}
async function submitPerpOptions(e) {
e.preventDefault();
const body = {
base: ($("calc-po-base") && $("calc-po-base").value) || "ETH",
spot: num("calc-po-spot"),
capital_usdt: num("calc-po-capital"),
target_profit_u: num("calc-po-target"),
move_mode: ($("calc-po-move-mode") && $("calc-po-move-mode").value) || "points",
move_value: num("calc-po-move"),
perp_leverage: num("calc-po-perp-lev"),
option_leverage: num("calc-po-opt-lev"),
ct_mult: num("calc-po-ct-mult") || 0.01,
};
try {
const r = await fetch("/api/calculator/perp-options", {
method: "POST",
credentials: "same-origin",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const j = await r.json();
if (!j.ok) {
showErr("calc-po-result", j.msg || "计算失败");
return;
}
renderPerpOptionsResult(j.data);
} catch (err) {
showErr("calc-po-result", String(err));
}
}
function syncPoMoveLabel() {
const mode = ($("calc-po-move-mode") && $("calc-po-move-mode").value) || "points";
const lab = $("calc-po-move-label");
if (lab) lab.textContent = mode === "pct" ? "波动率 %" : "波动点数";
}
function applyCalcTab(tab) {
const t = tab === "roll" ? "roll" : "trend";
const t = tab === "roll" || tab === "po" ? tab : "trend";
const layout = page.querySelector(".calc-layout");
if (layout) layout.setAttribute("data-calc-tab", t);
page.querySelectorAll(".calc-m-tab").forEach(function (btn) {
@@ -609,13 +736,20 @@
await loadCalculatorExchanges();
const trendForm = $("calc-trend-form");
const rollForm = $("calc-roll-form");
const poForm = $("calc-po-form");
const dirSel = $("calc-trend-direction");
const poMode = $("calc-po-move-mode");
if (trendForm) trendForm.addEventListener("submit", submitTrend);
if (rollForm) rollForm.addEventListener("submit", submitRoll);
if (poForm) poForm.addEventListener("submit", submitPerpOptions);
if (dirSel) {
dirSel.addEventListener("change", syncTrendAddLabel);
syncTrendAddLabel();
}
if (poMode) {
poMode.addEventListener("change", syncPoMoveLabel);
syncPoMoveLabel();
}
bindRollLegsUI();
bindMarket("calc-trend");
bindMarket("calc-roll");
+68 -2
View File
@@ -16,7 +16,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
<noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript>
<link rel="stylesheet" href="/assets/app.css?v=20260724-opt-archive" />
<link rel="stylesheet" href="/assets/app.css?v=20260728-po-calc" />
<link rel="stylesheet" href="/assets/trade_stats_calendar.css?v=4" />
<link rel="stylesheet" href="/assets/account_risk_badge.css?v=4" />
<script src="/assets/account_risk_badge.js?v=4"></script>
@@ -818,6 +818,9 @@
<button type="button" class="calc-m-tab" data-calc-tab="roll" role="tab" aria-selected="false">
<span class="calc-tab-label-mobile">滚仓</span><span class="calc-tab-label-desktop">滚仓计算器</span>
</button>
<button type="button" class="calc-m-tab" data-calc-tab="po" role="tab" aria-selected="false">
<span class="calc-tab-label-mobile">永期对冲</span><span class="calc-tab-label-desktop">永期对冲计算器</span>
</button>
</div>
<div class="calc-layout" data-calc-tab="trend">
<section class="calc-card card" data-calc-pane="trend">
@@ -958,6 +961,69 @@
</aside>
</div>
</section>
<section class="calc-card card" data-calc-pane="po">
<div class="calc-pane-split">
<div class="calc-input-panel">
<h2>永期对冲计算器</h2>
<p class="calc-hint">永续固定 1 币;单币权利金 = 现价 / 期权杠杆;权利金按全亏;只扣永续开平手续费(各 0.05%).主要反推期权开仓币数/张数.</p>
<form id="calc-po-form" class="calc-form">
<div class="calc-form-grid">
<label class="calc-field">
<span>币种</span>
<select id="calc-po-base">
<option value="ETH" selected>ETH</option>
<option value="BTC">BTC</option>
</select>
</label>
<label class="calc-field">
<span>现价</span>
<input id="calc-po-spot" type="number" min="0" step="any" value="1800" required />
</label>
<label class="calc-field">
<span>交易资金 (U·参考)</span>
<input id="calc-po-capital" type="number" min="0.01" step="any" value="3000" required />
</label>
<label class="calc-field">
<span>目标盈利 (U)</span>
<input id="calc-po-target" type="number" min="0" step="any" value="15" required />
</label>
<label class="calc-field">
<span>波动模式</span>
<select id="calc-po-move-mode">
<option value="points" selected>波动点数</option>
<option value="pct">波动率 %</option>
</select>
</label>
<label class="calc-field">
<span id="calc-po-move-label">波动点数</span>
<input id="calc-po-move" type="number" min="0" step="any" value="50" required />
</label>
<label class="calc-field">
<span>永续杠杆</span>
<input id="calc-po-perp-lev" type="number" min="0.01" step="any" value="10" required />
</label>
<label class="calc-field">
<span>期权杠杆</span>
<input id="calc-po-opt-lev" type="number" min="0.01" step="any" value="100" required />
</label>
<label class="calc-field">
<span>合约乘数 ct_mult</span>
<input id="calc-po-ct-mult" type="number" min="0.0001" step="any" value="0.01" required />
</label>
</div>
<div class="calc-actions">
<button type="submit" class="primary">计算</button>
</div>
</form>
</div>
<aside class="calc-result-panel" aria-label="永期对冲结果推算">
<h3 class="calc-result-title">结果推算</h3>
<p class="calc-result-placeholder">填写左侧参数后点击「计算」</p>
<div id="calc-po-result" class="calc-result hidden"></div>
</aside>
</div>
</section>
</div>
</div>
</div>
@@ -1674,7 +1740,7 @@
<script src="/assets/chart_draw.js?v=20260720-option-day-1600"></script>
<script src="/assets/chart.js?v=20260720-option-day-1600"></script>
<script src="/assets/plan.js?v=20260720-autofill"></script>
<script src="/assets/calculator.js?v=20260715-calc-tabs"></script>
<script src="/assets/calculator.js?v=20260728-po-calc"></script>
<script src="/assets/compare.js?v=20260723-compare"></script>
<script src="/assets/trade_stats_calendar.js?v=3"></script>
<script src="/assets/archive.js?v=20260724-opt-archive"></script>