Add ratio-to-move-points mode for hub perpetual-options calculator.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
"""中控永期对冲计算器:永续 1 币 + 按目标盈利反推期权仓位(纯函数)."""
|
"""中控永期对冲计算器:永续 1 币 + 按目标盈利反推期权仓位/波动点数(纯函数)."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Optional, Tuple
|
from typing import Any, Optional, Tuple
|
||||||
@@ -18,6 +18,63 @@ def _f(v: Any) -> Optional[float]:
|
|||||||
return None
|
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(
|
def calc_perp_options_hedge(
|
||||||
*,
|
*,
|
||||||
base: str = "ETH",
|
base: str = "ETH",
|
||||||
@@ -30,60 +87,51 @@ def calc_perp_options_hedge(
|
|||||||
option_leverage: float,
|
option_leverage: float,
|
||||||
ct_mult: float = DEFAULT_CT_MULT,
|
ct_mult: float = DEFAULT_CT_MULT,
|
||||||
) -> Tuple[Optional[dict[str, Any]], Optional[str]]:
|
) -> 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()
|
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)
|
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()
|
mode = (move_mode or "points").strip().lower()
|
||||||
if mode not in ("points", "pct", "percent", "rate"):
|
if mode not in ("points", "pct", "percent", "rate"):
|
||||||
return None, "波动模式须为 points 或 pct"
|
return None, "波动模式须为 points 或 pct"
|
||||||
if mode in ("percent", "rate"):
|
if mode in ("percent", "rate"):
|
||||||
mode = "pct"
|
mode = "pct"
|
||||||
|
if move is None:
|
||||||
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, "参数格式错误"
|
return None, "参数格式错误"
|
||||||
if ct is None or ct <= 0:
|
if move <= 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"
|
return None, "现价、资金、波动、杠杆须大于 0"
|
||||||
if target < 0:
|
|
||||||
return None, "目标盈利不能为负"
|
|
||||||
|
|
||||||
# 波动对应的绝对点数(价格变动)
|
|
||||||
if mode == "pct":
|
if mode == "pct":
|
||||||
move_points = s * (move / 100.0)
|
move_points = s * (move / 100.0)
|
||||||
else:
|
else:
|
||||||
move_points = move
|
move_points = move
|
||||||
|
|
||||||
if move_points <= 0:
|
if move_points <= 0:
|
||||||
return None, "波动对应价格变动须大于 0"
|
return None, "波动对应价格变动须大于 0"
|
||||||
|
|
||||||
exit_px = s + move_points # 永续方向对:按上涨测算 1 币多头
|
exit_px = s + move_points
|
||||||
perp_gross = move_points * PERP_COINS
|
perp_gross = move_points * PERP_COINS
|
||||||
fee = estimate_roundtrip_fee_usdt(
|
fee = estimate_roundtrip_fee_usdt(s, exit_px, qty=PERP_COINS, contract_size=1.0)
|
||||||
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
|
premium_budget = perp_gross - target - fee
|
||||||
if premium_budget <= 0:
|
if premium_budget <= 0:
|
||||||
@@ -91,21 +139,16 @@ def calc_perp_options_hedge(
|
|||||||
|
|
||||||
opt_coins = premium_budget / prem_per_coin
|
opt_coins = premium_budget / prem_per_coin
|
||||||
opt_sheets = opt_coins / ct
|
opt_sheets = opt_coins / ct
|
||||||
premium_total = opt_coins * prem_per_coin # == premium_budget
|
premium_total = opt_coins * prem_per_coin
|
||||||
|
|
||||||
margin = (s * PERP_COINS) / p_lev
|
|
||||||
capital_ok = capital >= margin
|
|
||||||
|
|
||||||
# A: 永续方向对,期权权利金全亏
|
|
||||||
case_a_net = perp_gross - premium_total - fee
|
case_a_net = perp_gross - premium_total - fee
|
||||||
|
|
||||||
# B: 期权方向对,永续 1 币反向亏掉同等波动
|
|
||||||
opt_intrinsic = opt_coins * move_points
|
opt_intrinsic = opt_coins * move_points
|
||||||
opt_net = opt_intrinsic - premium_total
|
opt_net = opt_intrinsic - premium_total
|
||||||
perp_loss = -perp_gross
|
perp_loss = -perp_gross
|
||||||
portfolio_net = opt_net + perp_loss
|
portfolio_net = opt_net + perp_loss
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
"calc_mode": "size",
|
||||||
"base": b,
|
"base": b,
|
||||||
"spot": round(s, 8),
|
"spot": round(s, 8),
|
||||||
"capital_usdt": round(capital, 8),
|
"capital_usdt": round(capital, 8),
|
||||||
@@ -127,7 +170,7 @@ def calc_perp_options_hedge(
|
|||||||
"opt_sheets": round(opt_sheets, 8),
|
"opt_sheets": round(opt_sheets, 8),
|
||||||
"premium_total_u": round(premium_total, 8),
|
"premium_total_u": round(premium_total, 8),
|
||||||
"perp_margin_u": round(margin, 8),
|
"perp_margin_u": round(margin, 8),
|
||||||
"capital_ok": bool(capital_ok),
|
"capital_ok": bool(capital >= margin),
|
||||||
"case_a": {
|
"case_a": {
|
||||||
"label": "永续方向对",
|
"label": "永续方向对",
|
||||||
"perp_pnl_u": round(perp_gross, 8),
|
"perp_pnl_u": round(perp_gross, 8),
|
||||||
@@ -144,3 +187,155 @@ def calc_perp_options_hedge(
|
|||||||
"portfolio_net_u": round(portfolio_net, 8),
|
"portfolio_net_u": round(portfolio_net, 8),
|
||||||
},
|
},
|
||||||
}, None
|
}, 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)
|
||||||
|
|||||||
+34
-13
@@ -1270,15 +1270,18 @@ class RollCalculatorBody(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class PerpOptionsCalculatorBody(BaseModel):
|
class PerpOptionsCalculatorBody(BaseModel):
|
||||||
|
calc_mode: str = "size"
|
||||||
base: str = "ETH"
|
base: str = "ETH"
|
||||||
spot: float = Field(gt=0)
|
spot: float = Field(gt=0)
|
||||||
capital_usdt: float = Field(gt=0)
|
capital_usdt: float = Field(gt=0)
|
||||||
target_profit_u: float = Field(ge=0)
|
target_profit_u: float = Field(ge=0)
|
||||||
move_mode: str = "points"
|
move_mode: str = "points"
|
||||||
move_value: float = Field(gt=0)
|
move_value: float | None = None
|
||||||
perp_leverage: float = Field(gt=0)
|
perp_leverage: float = Field(gt=0)
|
||||||
option_leverage: float = Field(gt=0)
|
option_leverage: float = Field(gt=0)
|
||||||
ct_mult: float = Field(default=0.01, 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):
|
class CompareOptionLegBody(BaseModel):
|
||||||
@@ -1364,19 +1367,37 @@ def api_calculator_roll(body: RollCalculatorBody):
|
|||||||
|
|
||||||
@app.post("/api/calculator/perp-options")
|
@app.post("/api/calculator/perp-options")
|
||||||
def api_calculator_perp_options(body: PerpOptionsCalculatorBody):
|
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(
|
mode = (body.calc_mode or "size").strip().lower()
|
||||||
base=body.base,
|
if mode in ("points", "ratio", "move"):
|
||||||
spot=body.spot,
|
data, err = calc_perp_options(
|
||||||
capital_usdt=body.capital_usdt,
|
calc_mode="points",
|
||||||
target_profit_u=body.target_profit_u,
|
base=body.base,
|
||||||
move_mode=body.move_mode,
|
spot=body.spot,
|
||||||
move_value=body.move_value,
|
capital_usdt=body.capital_usdt,
|
||||||
perp_leverage=body.perp_leverage,
|
target_profit_u=body.target_profit_u,
|
||||||
option_leverage=body.option_leverage,
|
perp_leverage=body.perp_leverage,
|
||||||
ct_mult=body.ct_mult,
|
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:
|
if err:
|
||||||
return JSONResponse({"ok": False, "msg": err}, status_code=400)
|
return JSONResponse({"ok": False, "msg": err}, status_code=400)
|
||||||
return {"ok": True, "data": data}
|
return {"ok": True, "data": data}
|
||||||
|
|||||||
@@ -575,6 +575,10 @@
|
|||||||
function renderPerpOptionsResult(data) {
|
function renderPerpOptionsResult(data) {
|
||||||
const box = $("calc-po-result");
|
const box = $("calc-po-result");
|
||||||
if (!box) return;
|
if (!box) return;
|
||||||
|
if ((data.calc_mode || "size") === "points") {
|
||||||
|
renderPerpOptionsPointsResult(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const a = data.case_a || {};
|
const a = data.case_a || {};
|
||||||
const b = data.case_b || {};
|
const b = data.case_b || {};
|
||||||
const capitalHint = data.capital_ok
|
const capitalHint = data.capital_ok
|
||||||
@@ -662,9 +666,124 @@
|
|||||||
"</div></section></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">' +
|
||||||
|
"<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) {
|
async function submitPerpOptions(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
const calcMode = ($("calc-po-calc-mode") && $("calc-po-calc-mode").value) || "size";
|
||||||
const body = {
|
const body = {
|
||||||
|
calc_mode: calcMode,
|
||||||
base: ($("calc-po-base") && $("calc-po-base").value) || "ETH",
|
base: ($("calc-po-base") && $("calc-po-base").value) || "ETH",
|
||||||
spot: num("calc-po-spot"),
|
spot: num("calc-po-spot"),
|
||||||
capital_usdt: num("calc-po-capital"),
|
capital_usdt: num("calc-po-capital"),
|
||||||
@@ -674,6 +793,8 @@
|
|||||||
perp_leverage: num("calc-po-perp-lev"),
|
perp_leverage: num("calc-po-perp-lev"),
|
||||||
option_leverage: num("calc-po-opt-lev"),
|
option_leverage: num("calc-po-opt-lev"),
|
||||||
ct_mult: num("calc-po-ct-mult") || 0.01,
|
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 {
|
try {
|
||||||
const r = await fetch("/api/calculator/perp-options", {
|
const r = await fetch("/api/calculator/perp-options", {
|
||||||
@@ -699,6 +820,22 @@
|
|||||||
if (lab) lab.textContent = mode === "pct" ? "波动率 %" : "波动点数";
|
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) {
|
function applyCalcTab(tab) {
|
||||||
const t = tab === "roll" || tab === "po" ? tab : "trend";
|
const t = tab === "roll" || tab === "po" ? tab : "trend";
|
||||||
const layout = page.querySelector(".calc-layout");
|
const layout = page.querySelector(".calc-layout");
|
||||||
@@ -739,6 +876,7 @@
|
|||||||
const poForm = $("calc-po-form");
|
const poForm = $("calc-po-form");
|
||||||
const dirSel = $("calc-trend-direction");
|
const dirSel = $("calc-trend-direction");
|
||||||
const poMode = $("calc-po-move-mode");
|
const poMode = $("calc-po-move-mode");
|
||||||
|
const poCalcMode = $("calc-po-calc-mode");
|
||||||
if (trendForm) trendForm.addEventListener("submit", submitTrend);
|
if (trendForm) trendForm.addEventListener("submit", submitTrend);
|
||||||
if (rollForm) rollForm.addEventListener("submit", submitRoll);
|
if (rollForm) rollForm.addEventListener("submit", submitRoll);
|
||||||
if (poForm) poForm.addEventListener("submit", submitPerpOptions);
|
if (poForm) poForm.addEventListener("submit", submitPerpOptions);
|
||||||
@@ -750,6 +888,10 @@
|
|||||||
poMode.addEventListener("change", syncPoMoveLabel);
|
poMode.addEventListener("change", syncPoMoveLabel);
|
||||||
syncPoMoveLabel();
|
syncPoMoveLabel();
|
||||||
}
|
}
|
||||||
|
if (poCalcMode) {
|
||||||
|
poCalcMode.addEventListener("change", syncPoCalcMode);
|
||||||
|
syncPoCalcMode();
|
||||||
|
}
|
||||||
bindRollLegsUI();
|
bindRollLegsUI();
|
||||||
bindMarket("calc-trend");
|
bindMarket("calc-trend");
|
||||||
bindMarket("calc-roll");
|
bindMarket("calc-roll");
|
||||||
|
|||||||
@@ -966,9 +966,16 @@
|
|||||||
<div class="calc-pane-split">
|
<div class="calc-pane-split">
|
||||||
<div class="calc-input-panel">
|
<div class="calc-input-panel">
|
||||||
<h2>永期对冲计算器</h2>
|
<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">
|
<form id="calc-po-form" class="calc-form">
|
||||||
<div class="calc-form-grid">
|
<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">
|
<label class="calc-field">
|
||||||
<span>币种</span>
|
<span>币种</span>
|
||||||
<select id="calc-po-base">
|
<select id="calc-po-base">
|
||||||
@@ -988,16 +995,24 @@
|
|||||||
<span>目标盈利 (U)</span>
|
<span>目标盈利 (U)</span>
|
||||||
<input id="calc-po-target" type="number" min="0" step="any" value="15" required />
|
<input id="calc-po-target" type="number" min="0" step="any" value="15" required />
|
||||||
</label>
|
</label>
|
||||||
<label class="calc-field">
|
<label class="calc-field calc-po-size-only">
|
||||||
<span>波动模式</span>
|
<span>波动模式</span>
|
||||||
<select id="calc-po-move-mode">
|
<select id="calc-po-move-mode">
|
||||||
<option value="points" selected>波动点数</option>
|
<option value="points" selected>波动点数</option>
|
||||||
<option value="pct">波动率 %</option>
|
<option value="pct">波动率 %</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label class="calc-field">
|
<label class="calc-field calc-po-size-only">
|
||||||
<span id="calc-po-move-label">波动点数</span>
|
<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>
|
||||||
<label class="calc-field">
|
<label class="calc-field">
|
||||||
<span>永续杠杆</span>
|
<span>永续杠杆</span>
|
||||||
@@ -1740,7 +1755,7 @@
|
|||||||
<script src="/assets/chart_draw.js?v=20260720-option-day-1600"></script>
|
<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/chart.js?v=20260720-option-day-1600"></script>
|
||||||
<script src="/assets/plan.js?v=20260720-autofill"></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/compare.js?v=20260723-compare"></script>
|
||||||
<script src="/assets/trade_stats_calendar.js?v=3"></script>
|
<script src="/assets/trade_stats_calendar.js?v=3"></script>
|
||||||
<script src="/assets/archive.js?v=20260724-opt-archive"></script>
|
<script src="/assets/archive.js?v=20260724-opt-archive"></script>
|
||||||
|
|||||||
@@ -2,7 +2,11 @@
|
|||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
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,
|
||||||
|
calc_perp_options_hedge,
|
||||||
|
calc_perp_options_points,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class HubPerpOptionsCalcTests(unittest.TestCase):
|
class HubPerpOptionsCalcTests(unittest.TestCase):
|
||||||
@@ -81,6 +85,52 @@ class HubPerpOptionsCalcTests(unittest.TestCase):
|
|||||||
self.assertIsNone(data)
|
self.assertIsNone(data)
|
||||||
self.assertIsNotNone(err)
|
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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user