Add hub perpetual-options hedge calculator tab and sizing formula.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
"""中控永期对冲计算器:永续 1 币 + 按目标盈利反推期权仓位(纯函数)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Optional, Tuple
|
||||
|
||||
from lib.trade.trade_fee_lib import estimate_roundtrip_fee_usdt, taker_fee_rate
|
||||
|
||||
DEFAULT_CT_MULT = 0.01
|
||||
PERP_COINS = 1.0
|
||||
|
||||
|
||||
def _f(v: Any) -> Optional[float]:
|
||||
if v is None or v == "":
|
||||
return None
|
||||
try:
|
||||
return float(v)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def calc_perp_options_hedge(
|
||||
*,
|
||||
base: str = "ETH",
|
||||
spot: float,
|
||||
capital_usdt: float,
|
||||
target_profit_u: float,
|
||||
move_mode: str = "points",
|
||||
move_value: float,
|
||||
perp_leverage: float,
|
||||
option_leverage: float,
|
||||
ct_mult: float = DEFAULT_CT_MULT,
|
||||
) -> Tuple[Optional[dict[str, Any]], Optional[str]]:
|
||||
"""测算期权开仓币数/张数,并给出永续对 / 期权对两套情景.
|
||||
|
||||
单币权利金 = 现价 / 期权杠杆
|
||||
权利金预算 = 永续毛收益 − 目标盈利 − 永续开平手续费
|
||||
期权币数 = 权利金预算 / 单币权利金
|
||||
"""
|
||||
b = (base or "ETH").strip().upper()
|
||||
if b not in ("ETH", "BTC"):
|
||||
return None, "币种仅支持 BTC / ETH"
|
||||
|
||||
s = _f(spot)
|
||||
capital = _f(capital_usdt)
|
||||
target = _f(target_profit_u)
|
||||
move = _f(move_value)
|
||||
p_lev = _f(perp_leverage)
|
||||
o_lev = _f(option_leverage)
|
||||
ct = _f(ct_mult)
|
||||
mode = (move_mode or "points").strip().lower()
|
||||
if mode not in ("points", "pct", "percent", "rate"):
|
||||
return None, "波动模式须为 points 或 pct"
|
||||
if mode in ("percent", "rate"):
|
||||
mode = "pct"
|
||||
|
||||
if s is None or capital is None or target is None or move is None or p_lev is None or o_lev is None:
|
||||
return None, "参数格式错误"
|
||||
if ct is None or ct <= 0:
|
||||
ct = DEFAULT_CT_MULT
|
||||
if s <= 0 or capital <= 0 or move <= 0 or p_lev <= 0 or o_lev <= 0:
|
||||
return None, "现价、资金、波动、杠杆须大于 0"
|
||||
if target < 0:
|
||||
return None, "目标盈利不能为负"
|
||||
|
||||
# 波动对应的绝对点数(价格变动)
|
||||
if mode == "pct":
|
||||
move_points = s * (move / 100.0)
|
||||
else:
|
||||
move_points = move
|
||||
|
||||
if move_points <= 0:
|
||||
return None, "波动对应价格变动须大于 0"
|
||||
|
||||
exit_px = s + move_points # 永续方向对:按上涨测算 1 币多头
|
||||
perp_gross = move_points * PERP_COINS
|
||||
fee = estimate_roundtrip_fee_usdt(
|
||||
s,
|
||||
exit_px,
|
||||
qty=PERP_COINS,
|
||||
contract_size=1.0,
|
||||
)
|
||||
fee_rate = taker_fee_rate()
|
||||
|
||||
prem_per_coin = s / o_lev
|
||||
if prem_per_coin <= 0:
|
||||
return None, "单币权利金无效"
|
||||
|
||||
premium_budget = perp_gross - target - fee
|
||||
if premium_budget <= 0:
|
||||
return None, "波动收益不足以覆盖目标盈利+手续费,无法开期权"
|
||||
|
||||
opt_coins = premium_budget / prem_per_coin
|
||||
opt_sheets = opt_coins / ct
|
||||
premium_total = opt_coins * prem_per_coin # == premium_budget
|
||||
|
||||
margin = (s * PERP_COINS) / p_lev
|
||||
capital_ok = capital >= margin
|
||||
|
||||
# A: 永续方向对,期权权利金全亏
|
||||
case_a_net = perp_gross - premium_total - fee
|
||||
|
||||
# B: 期权方向对,永续 1 币反向亏掉同等波动
|
||||
opt_intrinsic = opt_coins * move_points
|
||||
opt_net = opt_intrinsic - premium_total
|
||||
perp_loss = -perp_gross
|
||||
portfolio_net = opt_net + perp_loss
|
||||
|
||||
return {
|
||||
"base": b,
|
||||
"spot": round(s, 8),
|
||||
"capital_usdt": round(capital, 8),
|
||||
"target_profit_u": round(target, 8),
|
||||
"move_mode": mode,
|
||||
"move_value": round(move, 8),
|
||||
"move_points": round(move_points, 8),
|
||||
"exit_price": round(exit_px, 8),
|
||||
"perp_coins": PERP_COINS,
|
||||
"perp_leverage": round(p_lev, 8),
|
||||
"option_leverage": round(o_lev, 8),
|
||||
"ct_mult": ct,
|
||||
"prem_per_coin": round(prem_per_coin, 8),
|
||||
"perp_gross_u": round(perp_gross, 8),
|
||||
"perp_fee_u": round(fee, 8),
|
||||
"fee_rate": fee_rate,
|
||||
"premium_budget_u": round(premium_budget, 8),
|
||||
"opt_coins": round(opt_coins, 8),
|
||||
"opt_sheets": round(opt_sheets, 8),
|
||||
"premium_total_u": round(premium_total, 8),
|
||||
"perp_margin_u": round(margin, 8),
|
||||
"capital_ok": bool(capital_ok),
|
||||
"case_a": {
|
||||
"label": "永续方向对",
|
||||
"perp_pnl_u": round(perp_gross, 8),
|
||||
"premium_u": round(premium_total, 8),
|
||||
"fee_u": round(fee, 8),
|
||||
"net_u": round(case_a_net, 8),
|
||||
},
|
||||
"case_b": {
|
||||
"label": "期权方向对",
|
||||
"opt_intrinsic_u": round(opt_intrinsic, 8),
|
||||
"premium_u": round(premium_total, 8),
|
||||
"opt_net_u": round(opt_net, 8),
|
||||
"perp_pnl_u": round(perp_loss, 8),
|
||||
"portfolio_net_u": round(portfolio_net, 8),
|
||||
},
|
||||
}, None
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
"""hub_perp_options_calc_lib 永期对冲测算."""
|
||||
|
||||
import unittest
|
||||
|
||||
from lib.hub.hub_perp_options_calc_lib import calc_perp_options_hedge
|
||||
|
||||
|
||||
class HubPerpOptionsCalcTests(unittest.TestCase):
|
||||
def test_example_1800_50_15_100(self):
|
||||
data, err = calc_perp_options_hedge(
|
||||
base="ETH",
|
||||
spot=1800,
|
||||
capital_usdt=3000,
|
||||
target_profit_u=15,
|
||||
move_mode="points",
|
||||
move_value=50,
|
||||
perp_leverage=10,
|
||||
option_leverage=100,
|
||||
ct_mult=0.01,
|
||||
)
|
||||
self.assertIsNone(err)
|
||||
assert data is not None
|
||||
self.assertEqual(data["prem_per_coin"], 18.0)
|
||||
self.assertEqual(data["perp_gross_u"], 50.0)
|
||||
# fee = (1800+1850)*0.0005 = 1.825
|
||||
self.assertAlmostEqual(data["perp_fee_u"], 1.825, places=6)
|
||||
self.assertAlmostEqual(data["premium_budget_u"], 33.175, places=6)
|
||||
self.assertAlmostEqual(data["opt_coins"], 33.175 / 18.0, places=6)
|
||||
self.assertAlmostEqual(data["opt_sheets"], data["opt_coins"] / 0.01, places=4)
|
||||
self.assertAlmostEqual(data["case_a"]["net_u"], 15.0, places=6)
|
||||
# B: opt_net = coins*50 - coins*18 = coins*32; portfolio = opt_net - 50
|
||||
coins = data["opt_coins"]
|
||||
self.assertAlmostEqual(data["case_b"]["opt_net_u"], coins * 32.0, places=6)
|
||||
self.assertAlmostEqual(data["case_b"]["portfolio_net_u"], coins * 32.0 - 50.0, places=6)
|
||||
self.assertAlmostEqual(data["perp_margin_u"], 180.0, places=6)
|
||||
self.assertTrue(data["capital_ok"])
|
||||
|
||||
def test_pct_mode(self):
|
||||
data, err = calc_perp_options_hedge(
|
||||
base="BTC",
|
||||
spot=100000,
|
||||
capital_usdt=5000,
|
||||
target_profit_u=100,
|
||||
move_mode="pct",
|
||||
move_value=1,
|
||||
perp_leverage=5,
|
||||
option_leverage=50,
|
||||
ct_mult=0.01,
|
||||
)
|
||||
self.assertIsNone(err)
|
||||
assert data is not None
|
||||
self.assertAlmostEqual(data["move_points"], 1000.0, places=6)
|
||||
self.assertAlmostEqual(data["perp_gross_u"], 1000.0, places=6)
|
||||
self.assertAlmostEqual(data["prem_per_coin"], 2000.0, places=6)
|
||||
|
||||
def test_budget_too_small(self):
|
||||
data, err = calc_perp_options_hedge(
|
||||
base="ETH",
|
||||
spot=1800,
|
||||
capital_usdt=3000,
|
||||
target_profit_u=60,
|
||||
move_mode="points",
|
||||
move_value=50,
|
||||
perp_leverage=10,
|
||||
option_leverage=100,
|
||||
)
|
||||
self.assertIsNone(data)
|
||||
self.assertIn("无法开期权", err or "")
|
||||
|
||||
def test_bad_base(self):
|
||||
data, err = calc_perp_options_hedge(
|
||||
base="SOL",
|
||||
spot=100,
|
||||
capital_usdt=1000,
|
||||
target_profit_u=10,
|
||||
move_mode="points",
|
||||
move_value=5,
|
||||
perp_leverage=5,
|
||||
option_leverage=20,
|
||||
)
|
||||
self.assertIsNone(data)
|
||||
self.assertIsNotNone(err)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user