Add ratio-to-move-points mode for hub perpetual-options calculator.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+34
-13
@@ -1270,15 +1270,18 @@ class RollCalculatorBody(BaseModel):
|
||||
|
||||
|
||||
class PerpOptionsCalculatorBody(BaseModel):
|
||||
calc_mode: str = "size"
|
||||
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)
|
||||
move_value: float | None = None
|
||||
perp_leverage: float = Field(gt=0)
|
||||
option_leverage: float = Field(gt=0)
|
||||
ct_mult: float = Field(default=0.01, gt=0)
|
||||
ratio_perp: float = Field(default=1.0, gt=0)
|
||||
ratio_opt: float = Field(default=2.0, gt=0)
|
||||
|
||||
|
||||
class CompareOptionLegBody(BaseModel):
|
||||
@@ -1364,19 +1367,37 @@ def api_calculator_roll(body: RollCalculatorBody):
|
||||
|
||||
@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
|
||||
from lib.hub.hub_perp_options_calc_lib import calc_perp_options
|
||||
|
||||
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,
|
||||
)
|
||||
mode = (body.calc_mode or "size").strip().lower()
|
||||
if mode in ("points", "ratio", "move"):
|
||||
data, err = calc_perp_options(
|
||||
calc_mode="points",
|
||||
base=body.base,
|
||||
spot=body.spot,
|
||||
capital_usdt=body.capital_usdt,
|
||||
target_profit_u=body.target_profit_u,
|
||||
perp_leverage=body.perp_leverage,
|
||||
option_leverage=body.option_leverage,
|
||||
ct_mult=body.ct_mult,
|
||||
ratio_perp=body.ratio_perp,
|
||||
ratio_opt=body.ratio_opt,
|
||||
)
|
||||
else:
|
||||
if body.move_value is None or body.move_value <= 0:
|
||||
return JSONResponse({"ok": False, "msg": "请填写波动数值"}, status_code=400)
|
||||
data, err = calc_perp_options(
|
||||
calc_mode="size",
|
||||
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}
|
||||
|
||||
@@ -575,6 +575,10 @@
|
||||
function renderPerpOptionsResult(data) {
|
||||
const box = $("calc-po-result");
|
||||
if (!box) return;
|
||||
if ((data.calc_mode || "size") === "points") {
|
||||
renderPerpOptionsPointsResult(data);
|
||||
return;
|
||||
}
|
||||
const a = data.case_a || {};
|
||||
const b = data.case_b || {};
|
||||
const capitalHint = data.capital_ok
|
||||
@@ -662,9 +666,124 @@
|
||||
"</div></section></div>";
|
||||
}
|
||||
|
||||
function renderPerpOptionsPointsResult(data) {
|
||||
const box = $("calc-po-result");
|
||||
if (!box) return;
|
||||
const a = data.case_a || {};
|
||||
const b = data.case_b || {};
|
||||
const capitalHint = data.capital_ok
|
||||
? "资金充足(参考)"
|
||||
: "保证金高于交易资金(仅提示)";
|
||||
let caseB =
|
||||
'<section class="calc-po-case">' +
|
||||
"<h4>情景 B · 期权方向对</h4>" +
|
||||
'<div class="calc-summary">' +
|
||||
"<div><span>所需点数(期权净利=目标)</span><strong>" +
|
||||
fmt(b.move_points_opt_net, 4) +
|
||||
" · " +
|
||||
fmt(b.move_pct_opt_net, 4) +
|
||||
"%</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.portfolio_net_at_opt_target_u) +
|
||||
'">' +
|
||||
fmtU(b.portfolio_net_at_opt_target_u) +
|
||||
"</strong></div>";
|
||||
if (b.move_points_portfolio != null) {
|
||||
caseB +=
|
||||
"<div><span>所需点数(组合净利=目标)</span><strong>" +
|
||||
fmt(b.move_points_portfolio, 4) +
|
||||
" · " +
|
||||
fmt(b.move_pct_portfolio, 4) +
|
||||
"%</strong></div>" +
|
||||
"<div><span>组合净利</span><strong class=\"" +
|
||||
pnlClass(b.portfolio_net_u) +
|
||||
'">' +
|
||||
fmtU(b.portfolio_net_u) +
|
||||
"</strong></div>" +
|
||||
"<div><span>此时期权净利</span><strong class=\"" +
|
||||
pnlClass(b.opt_net_at_portfolio_target_u) +
|
||||
'">' +
|
||||
fmtU(b.opt_net_at_portfolio_target_u) +
|
||||
"</strong></div>";
|
||||
} else if (b.portfolio_error) {
|
||||
caseB +=
|
||||
'<div class="calc-field-span2"><span>组合达目标</span><strong class="calc-market-err">' +
|
||||
esc(b.portfolio_error) +
|
||||
"</strong></div>";
|
||||
}
|
||||
caseB += "</div></section>";
|
||||
|
||||
box.classList.remove("hidden");
|
||||
box.innerHTML =
|
||||
'<div class="calc-summary">' +
|
||||
"<div><span>标的</span><strong>" +
|
||||
esc(data.base || "—") +
|
||||
" · 比例 " +
|
||||
esc(data.ratio_label || "—") +
|
||||
"</strong></div>" +
|
||||
"<div><span>仓位</span><strong>永续 " +
|
||||
fmtTrim(data.perp_coins, 4) +
|
||||
" 币 / 期权 " +
|
||||
fmtTrim(data.opt_coins, 4) +
|
||||
" 币(" +
|
||||
fmtTrim(data.opt_sheets, 2) +
|
||||
" 张)</strong></div>" +
|
||||
"<div><span>单币权利金</span><strong>" +
|
||||
fmt(data.prem_per_coin, 4) +
|
||||
"U</strong></div>" +
|
||||
"<div><span>权利金总额</span><strong>" +
|
||||
fmt(data.premium_total_u, 4) +
|
||||
"U</strong></div>" +
|
||||
"<div><span>目标盈利</span><strong>" +
|
||||
fmt(data.target_profit_u, 2) +
|
||||
"U</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>" +
|
||||
fmt(a.move_points, 4) +
|
||||
" · " +
|
||||
fmt(a.move_pct, 4) +
|
||||
"%</strong></div>" +
|
||||
"<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>" +
|
||||
caseB +
|
||||
"</div>";
|
||||
}
|
||||
|
||||
async function submitPerpOptions(e) {
|
||||
e.preventDefault();
|
||||
const calcMode = ($("calc-po-calc-mode") && $("calc-po-calc-mode").value) || "size";
|
||||
const body = {
|
||||
calc_mode: calcMode,
|
||||
base: ($("calc-po-base") && $("calc-po-base").value) || "ETH",
|
||||
spot: num("calc-po-spot"),
|
||||
capital_usdt: num("calc-po-capital"),
|
||||
@@ -674,6 +793,8 @@
|
||||
perp_leverage: num("calc-po-perp-lev"),
|
||||
option_leverage: num("calc-po-opt-lev"),
|
||||
ct_mult: num("calc-po-ct-mult") || 0.01,
|
||||
ratio_perp: num("calc-po-ratio-perp") || 1,
|
||||
ratio_opt: num("calc-po-ratio-opt") || 2,
|
||||
};
|
||||
try {
|
||||
const r = await fetch("/api/calculator/perp-options", {
|
||||
@@ -699,6 +820,22 @@
|
||||
if (lab) lab.textContent = mode === "pct" ? "波动率 %" : "波动点数";
|
||||
}
|
||||
|
||||
function syncPoCalcMode() {
|
||||
const mode = ($("calc-po-calc-mode") && $("calc-po-calc-mode").value) || "size";
|
||||
const points = mode === "points";
|
||||
page.querySelectorAll(".calc-po-size-only").forEach(function (el) {
|
||||
el.classList.toggle("hidden", points);
|
||||
});
|
||||
page.querySelectorAll(".calc-po-points-only").forEach(function (el) {
|
||||
el.classList.toggle("hidden", !points);
|
||||
});
|
||||
const moveInput = $("calc-po-move");
|
||||
if (moveInput) {
|
||||
if (points) moveInput.removeAttribute("required");
|
||||
else moveInput.setAttribute("required", "required");
|
||||
}
|
||||
}
|
||||
|
||||
function applyCalcTab(tab) {
|
||||
const t = tab === "roll" || tab === "po" ? tab : "trend";
|
||||
const layout = page.querySelector(".calc-layout");
|
||||
@@ -739,6 +876,7 @@
|
||||
const poForm = $("calc-po-form");
|
||||
const dirSel = $("calc-trend-direction");
|
||||
const poMode = $("calc-po-move-mode");
|
||||
const poCalcMode = $("calc-po-calc-mode");
|
||||
if (trendForm) trendForm.addEventListener("submit", submitTrend);
|
||||
if (rollForm) rollForm.addEventListener("submit", submitRoll);
|
||||
if (poForm) poForm.addEventListener("submit", submitPerpOptions);
|
||||
@@ -750,6 +888,10 @@
|
||||
poMode.addEventListener("change", syncPoMoveLabel);
|
||||
syncPoMoveLabel();
|
||||
}
|
||||
if (poCalcMode) {
|
||||
poCalcMode.addEventListener("change", syncPoCalcMode);
|
||||
syncPoCalcMode();
|
||||
}
|
||||
bindRollLegsUI();
|
||||
bindMarket("calc-trend");
|
||||
bindMarket("calc-roll");
|
||||
|
||||
@@ -966,9 +966,16 @@
|
||||
<div class="calc-pane-split">
|
||||
<div class="calc-input-panel">
|
||||
<h2>永期对冲计算器</h2>
|
||||
<p class="calc-hint">永续固定 1 币;单币权利金 = 现价 / 期权杠杆;权利金按全亏;只扣永续开平手续费(各 0.05%).主要反推期权开仓币数/张数.</p>
|
||||
<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-calc-mode">
|
||||
<option value="size" selected>由波动推期权仓位</option>
|
||||
<option value="points">由比例推波动点数</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="calc-field">
|
||||
<span>币种</span>
|
||||
<select id="calc-po-base">
|
||||
@@ -988,16 +995,24 @@
|
||||
<span>目标盈利 (U)</span>
|
||||
<input id="calc-po-target" type="number" min="0" step="any" value="15" required />
|
||||
</label>
|
||||
<label class="calc-field">
|
||||
<label class="calc-field calc-po-size-only">
|
||||
<span>波动模式</span>
|
||||
<select id="calc-po-move-mode">
|
||||
<option value="points" selected>波动点数</option>
|
||||
<option value="pct">波动率 %</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="calc-field">
|
||||
<label class="calc-field calc-po-size-only">
|
||||
<span id="calc-po-move-label">波动点数</span>
|
||||
<input id="calc-po-move" type="number" min="0" step="any" value="50" required />
|
||||
<input id="calc-po-move" type="number" min="0" step="any" value="50" />
|
||||
</label>
|
||||
<label class="calc-field calc-po-points-only hidden">
|
||||
<span>永续比例</span>
|
||||
<input id="calc-po-ratio-perp" type="number" min="0.01" step="any" value="1" />
|
||||
</label>
|
||||
<label class="calc-field calc-po-points-only hidden">
|
||||
<span>期权比例</span>
|
||||
<input id="calc-po-ratio-opt" type="number" min="0.01" step="any" value="2" />
|
||||
</label>
|
||||
<label class="calc-field">
|
||||
<span>永续杠杆</span>
|
||||
@@ -1740,7 +1755,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=20260728-po-calc"></script>
|
||||
<script src="/assets/calculator.js?v=20260728-po-points"></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>
|
||||
|
||||
Reference in New Issue
Block a user