Use absolute coin counts in perp-options points mode.

Treat 2:4 as 2 perp + 4 option coins instead of normalizing to 1:2, and disable embed page caching so hub iframe picks up trade UI updates.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-29 22:54:16 +08:00
parent 9f3395de2f
commit d89aff3ad6
7 changed files with 116 additions and 44 deletions
+24 -12
View File
@@ -93,16 +93,19 @@ API`POST /api/calculator/perp-options`
---
## 模式二:由比例推波动点数(`calc_mode=points`
## 模式二:由币数推波动点数(`calc_mode=points`
已知永续:期权比例(如 **1:2**)、目标盈利、期权杠杆 → 反推两套情景要涨/跌多少点才能达到目标。
已知永续币数 / 期权币数(如 **1:2****2:4**)、目标盈利、期权杠杆 → 反推两套情景要涨/跌多少点才能达到目标。
**按绝对币数**,不再把输入归一到「永续 1 币」。填 2 与 4 → 永续 2 币 + 期权 4 币(权利金、保证金、手续费均按 2 倍于 1:2 放大;达同一目标盈利所需点数会变小)。
### 仓位
```text
永续币数 = 1
期权币数 = 1 × (期权比例 / 永续比例) # 1:2 → 2 币
永续币数 = 输入的永续币数
期权币数 = 输入的期权币数
权利金总额 = 期权币数 × (现价 / 期权杠杆)
永续保证金 = 现价 × 永续币数 / 永续杠杆
```
### 情景 A · 永续方向对
@@ -110,10 +113,10 @@ API`POST /api/calculator/perp-options`
净利 = 目标盈利:
```text
move 权利金 fee(move) = 目标
fee(move) = (2×现价 + move) × 0.05%
qty×move 权利金 fee(move,qty) = 目标
fee = (2×现价 + move) × qty × 0.05%
move = (目标 + 权利金 + 2×现价×0.05%) / (1 0.05%)
move = (目标 + 权利金 + 2×现价×qty×0.05%) / (qty × (1 0.05%))
```
### 情景 B · 期权方向对(以组合净利为准)
@@ -121,19 +124,19 @@ move = (目标 + 权利金 + 2×现价×0.05%) / (1 0.05%)
组合净利 = 目标盈利:
```text
组合 = 期权币数×move − 权利金 − 1×move
= move×(期权币数 − 1) 权利金
组合 = 期权币数×move − 权利金 − 永续币数×move
= move×(期权币数 − 永续币数) 权利金
move = (目标 + 权利金) / (期权币数 − 1)
move = (目标 + 权利金) / (期权币数 − 永续币数)
```
要求期权币数 > 永续币数(比例须使期权侧更重,如 1:2);若为 1:1,组合恒为 −权利金,无法解出正目标。
要求期权币数 > 永续币数;若相等,组合恒为 −权利金,无法解出正目标。
结果区展示:所需波动点数(及折合%)、组合净利、其中期权净利、其中永续盈亏。
### 手测示例
现价 1800、目标 15、期权杠杆 100、比例 1:2 → 权利金总额 36U
现价 1800、目标 15、期权杠杆 100、币数 1:2 → 权利金总额 36U
| 情景 | 所需点数(约) |
|------|----------------|
@@ -141,6 +144,15 @@ move = (目标 + 权利金) / (期权币数 − 1)
| B 组合净利=15 | 51.00 |
| C 横盘最大亏损 | 37.80(权利金 36 + 同价开平费 1.8) |
币数 **2:4**(权利金 72U、保证金 360U):
| 情景 | 约值 |
|------|------|
| 仓位 | 永续 2 币 / 期权 4 币(400 张) |
| A 所需点数 | ≈45.32 |
| B 组合达目标 | 43.50 |
| C 横盘最大亏损 | 75.60 |
---
## API 请求体(摘要)
+1
View File
@@ -294,6 +294,7 @@
async function fetchTabHtml(tab) {
const r = await fetch(embedPageUrl(tab), {
credentials: "same-origin",
cache: "no-store",
headers: { "X-Instance-Soft-Nav": "1" },
});
const ct = (r.headers.get("content-type") || "").toLowerCase();
+49 -24
View File
@@ -63,25 +63,41 @@ def _parse_base_common(
}, None
def _move_for_perp_correct(*, spot: float, target: float, premium: float, fee_rate: float) -> float:
"""净利 = move premium fee(move) = target → 解 move.
def _move_for_perp_correct(
*,
spot: float,
target: float,
premium: float,
fee_rate: float,
perp_coins: float = 1.0,
) -> float:
"""净利 = qty*move premium fee(move,qty) = target → 解 move.
fee = (2*spot + move) * fee_rate
move*(1-fee_rate) = target + premium + 2*spot*fee_rate
fee = (2*spot + move) * qty * fee_rate
qty*move*(1-fee_rate) = target + premium + 2*spot*qty*fee_rate
"""
denom = 1.0 - float(fee_rate)
qty = float(perp_coins)
if qty <= 0:
return 0.0
denom = qty * (1.0 - float(fee_rate))
if denom <= 0:
return 0.0
return (float(target) + float(premium) + 2.0 * float(spot) * float(fee_rate)) / denom
return (float(target) + float(premium) + 2.0 * float(spot) * qty * float(fee_rate)) / denom
def _case_sideways(*, spot: float, premium_total: float) -> dict[str, Any]:
def _case_sideways(
*,
spot: float,
premium_total: float,
perp_coins: float = 1.0,
) -> dict[str, Any]:
"""横盘/到期无方向:永续≈0,期权权利金全亏,另计永续开平同价手续费.
最大亏损(正数) = 权利金总额 + 开平手续费(exit=entry)
组合净利 = −最大亏损
"""
fee_flat = estimate_roundtrip_fee_usdt(spot, spot, qty=PERP_COINS, contract_size=1.0)
qty = float(perp_coins) if float(perp_coins) > 0 else PERP_COINS
fee_flat = estimate_roundtrip_fee_usdt(spot, spot, qty=qty, contract_size=1.0)
prem = float(premium_total)
max_loss = prem + float(fee_flat)
return {
@@ -221,12 +237,12 @@ def calc_perp_options_points(
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 币.
永续币数 = ratio_perp, 期权币数 = ratio_opt(按绝对币数,不再归一到 1 币).
例 2:4 → 永续 2 币 + 期权 4 币;1:2 → 永续 1 币 + 期权 2 币.
A 永续方向对: move premium fee(move) = 目标盈利
A 永续方向对: qty*move premium fee(move,qty) = 目标盈利
B 期权方向对:
- 期权净利达目标: opt_coins*move premium = 目标
- 组合净利达目标: move*(opt_coins perp_coins) premium = 目标
@@ -246,7 +262,7 @@ def calc_perp_options_points(
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"
return None, "永续/期权币数须大于 0"
s = common["spot"]
capital = common["capital"]
@@ -255,28 +271,35 @@ def calc_perp_options_points(
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)
perp_coins = rp
opt_coins = ro
premium_total = opt_coins * prem_per_coin
opt_sheets = opt_coins / ct
margin = (s * perp_coins) / p_lev
move_a = _move_for_perp_correct(spot=s, target=target, premium=premium_total, fee_rate=fee_rate)
move_a = _move_for_perp_correct(
spot=s,
target=target,
premium=premium_total,
fee_rate=fee_rate,
perp_coins=perp_coins,
)
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
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
portfolio_at_b_opt = opt_net_at_b_opt - move_b_opt * perp_coins
# 组合净利 = 目标
edge = opt_coins - PERP_COINS
edge = opt_coins - perp_coins
if edge <= 0:
move_b_port = None
port_err = "期权币数须大于永续币数,组合才能在方向对时赚到目标盈利"
@@ -285,7 +308,7 @@ def calc_perp_options_points(
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
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
@@ -299,7 +322,7 @@ def calc_perp_options_points(
"ratio_perp": round(rp, 8),
"ratio_opt": round(ro, 8),
"ratio_label": f"{_fmt_ratio(rp)}:{_fmt_ratio(ro)}",
"perp_coins": PERP_COINS,
"perp_coins": round(perp_coins, 8),
"opt_coins": round(opt_coins, 8),
"opt_sheets": round(opt_sheets, 8),
"perp_leverage": round(p_lev, 8),
@@ -314,7 +337,7 @@ def calc_perp_options_points(
"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),
"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),
@@ -336,7 +359,9 @@ def calc_perp_options_points(
"portfolio_error": port_err,
"premium_u": round(premium_total, 8),
},
"case_sideways": _case_sideways(spot=s, premium_total=premium_total),
"case_sideways": _case_sideways(
spot=s, premium_total=premium_total, perp_coins=perp_coins
),
}, None
+9 -3
View File
@@ -7,7 +7,7 @@ import os
from typing import Callable
from urllib.parse import parse_qsl, urlencode, urlsplit
from flask import Flask, Response, jsonify, redirect, request, session
from flask import Flask, Response, jsonify, make_response, redirect, request, session
from jinja2 import ChoiceLoader, FileSystemLoader
EMBED_TABS: tuple[str, ...] = (
@@ -184,7 +184,10 @@ def register_embed_routes(
if tab not in EMBED_TABS:
tab = "trade"
session["hub_embed_shell"] = True
return render_main_page_fn(tab, embed_mode="shell")
resp = make_response(render_main_page_fn(tab, embed_mode="shell"))
resp.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
resp.headers["Pragma"] = "no-cache"
return resp
@login_required
@app.route("/api/embed/page/<tab>")
@@ -198,7 +201,10 @@ def register_embed_routes(
html = render_main_page_fn(tab, embed_mode="fragment")
if isinstance(html, Response):
html = html.get_data(as_text=True)
return jsonify({"ok": True, "page": tab, "html": html})
resp = jsonify({"ok": True, "page": tab, "html": html})
resp.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
resp.headers["Pragma"] = "no-cache"
return resp
def pwa_app_name(exchange_key: str) -> str:
+1 -1
View File
@@ -168,7 +168,7 @@ window.__INSTANCE_DISPLAY__ = {{ display | tojson }};
</script>
<script src="/static/instance_settings_prefs.js?v=16"></script>
<script src="/static/instance_live.js?v=6"></script>
<script src="/static/instance_embed.js?v=29"></script>
<script src="/static/instance_embed.js?v=30"></script>
<script src="/static/instance_mobile_nav.js?v=2"></script>
</body>
</html>
+4 -4
View File
@@ -966,7 +966,7 @@
<div class="calc-pane-split">
<div class="calc-input-panel">
<h2>永期对冲计算器</h2>
<p class="calc-hint">永续固定 1 币;单币权利金 = 现价 / 期权杠杆;权利金按全亏;只扣永续开平手续费(各 0.05%).「推仓位」由波动反推期权数量;「推点数」按永续:期权比例反推达目标盈利所需波动.横盘最大亏损 = 权利金全亏 + 永续开平同价手续费.</p>
<p class="calc-hint">推点数模式:「永续币数/期权币数」按绝对仓位计算(填 2 与 4 → 永续 2 币 + 期权 4 币,不再归一成 1:2).单币权利金 = 现价 / 期权杠杆;权利金按全亏;只扣永续开平手续费(各 0.05%).「推仓位」仍固定永续 1 币、由波动反推期权数量.横盘最大亏损 = 权利金全亏 + 永续开平同价手续费.</p>
<form id="calc-po-form" class="calc-form">
<div class="calc-form-grid">
<label class="calc-field">
@@ -1007,11 +1007,11 @@
<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>
<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>
<span>期权币数</span>
<input id="calc-po-ratio-opt" type="number" min="0.01" step="any" value="2" />
</label>
<label class="calc-field">
@@ -1751,7 +1751,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=20260729-po-sideways"></script>
<script src="/assets/calculator.js?v=20260729-po-abs-coins"></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>
+28
View File
@@ -125,6 +125,34 @@ class HubPerpOptionsCalcTests(unittest.TestCase):
self.assertAlmostEqual(sw["max_loss_u"], 37.8, places=6)
self.assertAlmostEqual(sw["net_u"], -37.8, places=6)
def test_points_absolute_2_to_4(self):
# 2:4 按绝对币数,不再归一成 1:2
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=2,
ratio_opt=4,
ct_mult=0.01,
)
self.assertIsNone(err)
assert data is not None
self.assertEqual(data["perp_coins"], 2.0)
self.assertEqual(data["opt_coins"], 4.0)
self.assertEqual(data["premium_total_u"], 72.0)
self.assertAlmostEqual(data["perp_margin_u"], 360.0, places=6)
expect_a = (15 + 72 + 2 * 1800 * 2 * 0.0005) / (2 * 0.9995)
self.assertAlmostEqual(data["case_a"]["move_points"], expect_a, places=6)
self.assertAlmostEqual(data["case_a"]["net_u"], 15.0, places=5)
self.assertAlmostEqual(data["case_b"]["move_points_portfolio"], 43.5, places=6)
self.assertAlmostEqual(data["case_b"]["portfolio_net_u"], 15.0, places=6)
sw = data["case_sideways"]
self.assertAlmostEqual(sw["fee_u"], 3.6, places=6)
self.assertAlmostEqual(sw["max_loss_u"], 75.6, places=6)
def test_points_ratio_1_to_1_no_portfolio(self):
data, err = calc_perp_options(
calc_mode="points",