From d89aff3ad68bc13c016bcffa73ae2257ac503555 Mon Sep 17 00:00:00 2001 From: dekun Date: Wed, 29 Jul 2026 22:54:16 +0800 Subject: [PATCH] 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 --- docs/永期对冲计算器.md | 36 ++++++++---- lib/common/static/instance_embed.js | 1 + lib/hub/hub_perp_options_calc_lib.py | 73 +++++++++++++++++-------- lib/instance/instance_embed_lib.py | 12 +++- lib/instance/templates/embed_shell.html | 2 +- manual_trading_hub/static/index.html | 8 +-- tests/test_hub_perp_options_calc_lib.py | 28 ++++++++++ 7 files changed, 116 insertions(+), 44 deletions(-) diff --git a/docs/永期对冲计算器.md b/docs/永期对冲计算器.md index af2f403..dc6469d 100644 --- a/docs/永期对冲计算器.md +++ b/docs/永期对冲计算器.md @@ -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 请求体(摘要) diff --git a/lib/common/static/instance_embed.js b/lib/common/static/instance_embed.js index 6aa0fea..cff7115 100644 --- a/lib/common/static/instance_embed.js +++ b/lib/common/static/instance_embed.js @@ -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(); diff --git a/lib/hub/hub_perp_options_calc_lib.py b/lib/hub/hub_perp_options_calc_lib.py index 91b946a..070c9b3 100644 --- a/lib/hub/hub_perp_options_calc_lib.py +++ b/lib/hub/hub_perp_options_calc_lib.py @@ -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 diff --git a/lib/instance/instance_embed_lib.py b/lib/instance/instance_embed_lib.py index c139065..9fa673d 100644 --- a/lib/instance/instance_embed_lib.py +++ b/lib/instance/instance_embed_lib.py @@ -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/") @@ -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: diff --git a/lib/instance/templates/embed_shell.html b/lib/instance/templates/embed_shell.html index 5d2646e..80648a8 100644 --- a/lib/instance/templates/embed_shell.html +++ b/lib/instance/templates/embed_shell.html @@ -168,7 +168,7 @@ window.__INSTANCE_DISPLAY__ = {{ display | tojson }}; - + diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index 97ae43b..32ed73d 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -966,7 +966,7 @@

永期对冲计算器

-

永续固定 1 币;单币权利金 = 现价 / 期权杠杆;权利金按全亏;只扣永续开平手续费(各 0.05%).「推仓位」由波动反推期权数量;「推点数」按永续:期权比例反推达目标盈利所需波动.横盘最大亏损 = 权利金全亏 + 永续开平同价手续费.

+

推点数模式:「永续币数/期权币数」按绝对仓位计算(填 2 与 4 → 永续 2 币 + 期权 4 币,不再归一成 1:2).单币权利金 = 现价 / 期权杠杆;权利金按全亏;只扣永续开平手续费(各 0.05%).「推仓位」仍固定永续 1 币、由波动反推期权数量.横盘最大亏损 = 权利金全亏 + 永续开平同价手续费.