5 Commits

7 changed files with 919 additions and 6 deletions
+2 -1
View File
@@ -6,6 +6,7 @@
| 标签 | 指向提交 | 说明 |
|------|----------|------|
| `snapshot/20260727` | `f53f281` | 2026-07-27:实例手机壳(下单/关键位/期权)、著作权声明、托管合同(一用户一机)、服务说明与报价说明 |
| `snapshot/20260726-2` | `4a79e01` | 2026-07-26 午:执行手册脑图(业务主题)、`.xmind` 按二进制入库、去掉缩略图避免 Gitea raw 换行损坏 |
| `snapshot/20260726` | `a2075ba` | 2026-07-26:Gate划转币种大写修复、系统设置划转页签停留、自动划转账户/币种下拉默认、期权「按可用余额打满」=min(余额,单笔预算)及说明 |
| `snapshot/20260724` | `890659f` | 2026-07-24:执行手册v2(无对冲)、监控/策略页签显隐、内照明心期权档案同步、期权开平仓微信必发、实例导航显隐关键位/实盘下单等 |
@@ -31,7 +32,7 @@
git tag -l 'snapshot/*'
# 检出快照(只读查看,勿在此分支直接开发)
git checkout snapshot/20260726-2
git checkout snapshot/20260727
# 回到主线
git checkout main
+341
View File
@@ -0,0 +1,341 @@
"""中控永期对冲计算器:永续 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 _parse_base_common(
*,
base: str,
spot: Any,
capital_usdt: Any,
target_profit_u: Any,
perp_leverage: Any,
option_leverage: Any,
ct_mult: Any,
) -> Tuple[Optional[dict[str, float]], 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)
p_lev = _f(perp_leverage)
o_lev = _f(option_leverage)
ct = _f(ct_mult)
if s is None or capital is None or target 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 p_lev <= 0 or o_lev <= 0:
return None, "现价、资金、杠杆须大于 0"
if target < 0:
return None, "目标盈利不能为负"
prem_per_coin = s / o_lev
if prem_per_coin <= 0:
return None, "单币权利金无效"
margin = (s * PERP_COINS) / p_lev
return {
"base_ok": 1.0,
"spot": s,
"capital": capital,
"target": target,
"p_lev": p_lev,
"o_lev": o_lev,
"ct": ct,
"prem_per_coin": prem_per_coin,
"margin": margin,
"fee_rate": taker_fee_rate(),
}, None
def _move_for_perp_correct(*, spot: float, target: float, premium: float, fee_rate: float) -> float:
"""净利 = move premium fee(move) = target → 解 move.
fee = (2*spot + move) * fee_rate
move*(1-fee_rate) = target + premium + 2*spot*fee_rate
"""
denom = 1.0 - float(fee_rate)
if denom <= 0:
return 0.0
return (float(target) + float(premium) + 2.0 * float(spot) * float(fee_rate)) / denom
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]]:
"""由波动反推期权开仓币数/张数(calc_mode=size)."""
common, err = _parse_base_common(
base=base,
spot=spot,
capital_usdt=capital_usdt,
target_profit_u=target_profit_u,
perp_leverage=perp_leverage,
option_leverage=option_leverage,
ct_mult=ct_mult,
)
if err or not common:
return None, err
s = common["spot"]
capital = common["capital"]
target = common["target"]
p_lev = common["p_lev"]
o_lev = common["o_lev"]
ct = common["ct"]
prem_per_coin = common["prem_per_coin"]
margin = common["margin"]
fee_rate = common["fee_rate"]
b = (base or "ETH").strip().upper()
move = _f(move_value)
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 move is None:
return None, "参数格式错误"
if move <= 0:
return None, "现价、资金、波动、杠杆须大于 0"
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
perp_gross = move_points * PERP_COINS
fee = estimate_roundtrip_fee_usdt(s, exit_px, qty=PERP_COINS, contract_size=1.0)
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
case_a_net = perp_gross - premium_total - fee
opt_intrinsic = opt_coins * move_points
opt_net = opt_intrinsic - premium_total
perp_loss = -perp_gross
portfolio_net = opt_net + perp_loss
return {
"calc_mode": "size",
"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 >= margin),
"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
def calc_perp_options_points(
*,
base: str = "ETH",
spot: float,
capital_usdt: float,
target_profit_u: float,
perp_leverage: float,
option_leverage: float,
ratio_perp: float = 1.0,
ratio_opt: float = 2.0,
ct_mult: float = DEFAULT_CT_MULT,
) -> Tuple[Optional[dict[str, Any]], Optional[str]]:
"""按永续:期权比例 + 目标盈利,反推两套情景所需波动点数.
永续币数固定为 ratio 归一后的 1 币侧(perp_coins = PERP_COINS).
期权币数 = PERP_COINS * (ratio_opt / ratio_perp),例 1:2 → 2 币.
A 永续方向对: move premium fee(move) = 目标盈利
B 期权方向对:
- 期权净利达目标: opt_coins*move premium = 目标
- 组合净利达目标: move*(opt_coins perp_coins) premium = 目标
"""
common, err = _parse_base_common(
base=base,
spot=spot,
capital_usdt=capital_usdt,
target_profit_u=target_profit_u,
perp_leverage=perp_leverage,
option_leverage=option_leverage,
ct_mult=ct_mult,
)
if err or not common:
return None, err
rp = _f(ratio_perp)
ro = _f(ratio_opt)
if rp is None or ro is None or rp <= 0 or ro <= 0:
return None, "永续:期权比例须大于 0"
s = common["spot"]
capital = common["capital"]
target = common["target"]
p_lev = common["p_lev"]
o_lev = common["o_lev"]
ct = common["ct"]
prem_per_coin = common["prem_per_coin"]
margin = common["margin"]
fee_rate = common["fee_rate"]
b = (base or "ETH").strip().upper()
opt_coins = PERP_COINS * (ro / rp)
premium_total = opt_coins * prem_per_coin
opt_sheets = opt_coins / ct
move_a = _move_for_perp_correct(spot=s, target=target, premium=premium_total, fee_rate=fee_rate)
if move_a <= 0:
return None, "无法解出永续方向对所需点数"
fee_a = estimate_roundtrip_fee_usdt(s, s + move_a, qty=PERP_COINS, contract_size=1.0)
net_a = move_a * PERP_COINS - premium_total - fee_a
# 期权净利 = 目标
move_b_opt = (target + premium_total) / opt_coins
opt_net_at_b_opt = opt_coins * move_b_opt - premium_total
portfolio_at_b_opt = opt_net_at_b_opt - move_b_opt * PERP_COINS
# 组合净利 = 目标
edge = opt_coins - PERP_COINS
if edge <= 0:
move_b_port = None
port_err = "期权币数须大于永续币数,组合才能在方向对时赚到目标盈利"
else:
move_b_port = (target + premium_total) / edge
port_err = None
if move_b_port is not None:
opt_net_at_b_port = opt_coins * move_b_port - premium_total
portfolio_at_b_port = opt_net_at_b_port - move_b_port * PERP_COINS
else:
opt_net_at_b_port = None
portfolio_at_b_port = None
return {
"calc_mode": "points",
"base": b,
"spot": round(s, 8),
"capital_usdt": round(capital, 8),
"target_profit_u": round(target, 8),
"ratio_perp": round(rp, 8),
"ratio_opt": round(ro, 8),
"ratio_label": f"{_fmt_ratio(rp)}:{_fmt_ratio(ro)}",
"perp_coins": PERP_COINS,
"opt_coins": round(opt_coins, 8),
"opt_sheets": round(opt_sheets, 8),
"perp_leverage": round(p_lev, 8),
"option_leverage": round(o_lev, 8),
"ct_mult": ct,
"prem_per_coin": round(prem_per_coin, 8),
"premium_total_u": round(premium_total, 8),
"fee_rate": fee_rate,
"perp_margin_u": round(margin, 8),
"capital_ok": bool(capital >= margin),
"case_a": {
"label": "永续方向对",
"move_points": round(move_a, 8),
"move_pct": round(move_a / s * 100.0, 8),
"perp_pnl_u": round(move_a * PERP_COINS, 8),
"premium_u": round(premium_total, 8),
"fee_u": round(fee_a, 8),
"net_u": round(net_a, 8),
},
"case_b": {
"label": "期权方向对",
"move_points_opt_net": round(move_b_opt, 8),
"move_pct_opt_net": round(move_b_opt / s * 100.0, 8),
"opt_net_u": round(opt_net_at_b_opt, 8),
"portfolio_net_at_opt_target_u": round(portfolio_at_b_opt, 8),
"move_points_portfolio": None if move_b_port is None else round(move_b_port, 8),
"move_pct_portfolio": None
if move_b_port is None
else round(move_b_port / s * 100.0, 8),
"opt_net_at_portfolio_target_u": None
if opt_net_at_b_port is None
else round(opt_net_at_b_port, 8),
"portfolio_net_u": None if portfolio_at_b_port is None else round(portfolio_at_b_port, 8),
"portfolio_error": port_err,
"premium_u": round(premium_total, 8),
},
}, None
def _fmt_ratio(v: float) -> str:
if abs(v - round(v)) < 1e-9:
return str(int(round(v)))
s = f"{v:.4f}".rstrip("0").rstrip(".")
return s
def calc_perp_options(
*,
calc_mode: str = "size",
**kwargs: Any,
) -> Tuple[Optional[dict[str, Any]], Optional[str]]:
"""统一入口:size=由波动推仓位;points=由比例推点数."""
mode = (calc_mode or "size").strip().lower()
if mode in ("points", "ratio", "move"):
return calc_perp_options_points(**kwargs)
# size mode: ignore ratio kwargs if present
kwargs.pop("ratio_perp", None)
kwargs.pop("ratio_opt", None)
return calc_perp_options_hedge(**kwargs)
+53
View File
@@ -1269,6 +1269,21 @@ class RollCalculatorBody(BaseModel):
base: str = "ETH"
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 | 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):
opt_type: str = "C"
strike: float | None = None
@@ -1350,6 +1365,44 @@ 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
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}
@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;
+275 -1
View File
@@ -572,8 +572,270 @@
}
}
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
? "资金充足(参考)"
: "保证金高于交易资金(仅提示)";
box.classList.remove("hidden");
box.innerHTML =
'<div class="calc-summary">' +
"<div><span>标的</span><strong>" +
esc(data.base || "—") +
" · 永续 " +
fmt(data.perp_coins, 2) +
" 币</strong></div>" +
"<div><span>单币权利金</span><strong>" +
fmt(data.prem_per_coin, 2) +
"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, 2) +
"U</strong></div>" +
"<div><span>权利金预算</span><strong>" +
fmt(data.premium_budget_u, 2) +
"U</strong></div>" +
"<div><span>期权开仓</span><strong>" +
fmt(data.opt_coins, 2) +
" 币 / " +
fmt(data.opt_sheets, 2) +
" 张</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, 2) +
"U</strong></div>" +
"<div><span>手续费</span><strong>" +
fmt(a.fee_u, 2) +
"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, 2) +
"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>";
}
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">';
if (b.move_points_portfolio != null) {
caseB +=
"<div><span>所需波动点数</span><strong>" +
fmt(b.move_points_portfolio, 2) +
" · " +
fmt(b.move_pct_portfolio, 2) +
"%</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>" +
"<div><span>其中永续盈亏</span><strong class=\"" +
pnlClass(
b.portfolio_net_u != null && b.opt_net_at_portfolio_target_u != null
? Number(b.portfolio_net_u) - Number(b.opt_net_at_portfolio_target_u)
: null
) +
'">' +
fmtU(
b.portfolio_net_u != null && b.opt_net_at_portfolio_target_u != null
? Number(b.portfolio_net_u) - Number(b.opt_net_at_portfolio_target_u)
: null
) +
"</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>永续 " +
fmt(data.perp_coins, 2) +
" 币 / 期权 " +
fmt(data.opt_coins, 2) +
" 币(" +
fmt(data.opt_sheets, 2) +
" 张)</strong></div>" +
"<div><span>单币权利金</span><strong>" +
fmt(data.prem_per_coin, 2) +
"U</strong></div>" +
"<div><span>权利金总额</span><strong>" +
fmt(data.premium_total_u, 2) +
"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, 2) +
" · " +
fmt(a.move_pct, 2) +
"%</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, 2) +
"U</strong></div>" +
"<div><span>手续费</span><strong>" +
fmt(a.fee_u, 2) +
"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"),
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,
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", {
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 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" ? "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 +871,25 @@
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");
const poCalcMode = $("calc-po-calc-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();
}
if (poCalcMode) {
poCalcMode.addEventListener("change", syncPoCalcMode);
syncPoCalcMode();
}
bindRollLegsUI();
bindMarket("calc-trend");
bindMarket("calc-roll");
+83 -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,84 @@
</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-calc-mode">
<option value="size" selected>由波动推期权仓位</option>
<option value="points">由比例推波动点数</option>
</select>
</label>
<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 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 calc-po-size-only">
<span id="calc-po-move-label">波动点数</span>
<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>
<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 +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=20260715-calc-tabs"></script>
<script src="/assets/calculator.js?v=20260728-po-2dp"></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>
+136
View File
@@ -0,0 +1,136 @@
"""hub_perp_options_calc_lib 永期对冲测算."""
import unittest
from lib.hub.hub_perp_options_calc_lib import (
calc_perp_options,
calc_perp_options_hedge,
calc_perp_options_points,
)
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)
def test_points_ratio_1_to_2(self):
# spot=1800, optLev=100 → prem/coin=18; ratio 1:2 → opt=2, premium=36
# A: move = (15+36+2*1800*0.0005)/(1-0.0005) = (51+1.8)/0.9995
data, err = calc_perp_options_points(
base="ETH",
spot=1800,
capital_usdt=3000,
target_profit_u=15,
perp_leverage=10,
option_leverage=100,
ratio_perp=1,
ratio_opt=2,
ct_mult=0.01,
)
self.assertIsNone(err)
assert data is not None
self.assertEqual(data["calc_mode"], "points")
self.assertEqual(data["opt_coins"], 2.0)
self.assertEqual(data["premium_total_u"], 36.0)
expect_a = (15 + 36 + 2 * 1800 * 0.0005) / 0.9995
self.assertAlmostEqual(data["case_a"]["move_points"], expect_a, places=6)
self.assertAlmostEqual(data["case_a"]["net_u"], 15.0, places=5)
# 期权净利=目标: (15+36)/2 = 25.5
self.assertAlmostEqual(data["case_b"]["move_points_opt_net"], 25.5, places=6)
self.assertAlmostEqual(data["case_b"]["opt_net_u"], 15.0, places=6)
# 组合净利=目标: (15+36)/(2-1) = 51
self.assertAlmostEqual(data["case_b"]["move_points_portfolio"], 51.0, places=6)
self.assertAlmostEqual(data["case_b"]["portfolio_net_u"], 15.0, places=6)
def test_points_ratio_1_to_1_no_portfolio(self):
data, err = calc_perp_options(
calc_mode="points",
base="ETH",
spot=1800,
capital_usdt=3000,
target_profit_u=15,
perp_leverage=10,
option_leverage=100,
ratio_perp=1,
ratio_opt=1,
)
self.assertIsNone(err)
assert data is not None
self.assertIsNone(data["case_b"]["move_points_portfolio"])
self.assertIsNotNone(data["case_b"]["portfolio_error"])
if __name__ == "__main__":
unittest.main()