Add OKX hedge-plan P0: env group, preview page, and PnL scenario math.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-14 11:00:15 +08:00
parent d27ccbea8f
commit 1fa98425e7
18 changed files with 2232 additions and 1 deletions
+1
View File
@@ -0,0 +1 @@
# hedge_plan package
+320
View File
@@ -0,0 +1,320 @@
"""对冲计划:情景测算与全仓建议仓(纯函数,无 IO)."""
from __future__ import annotations
from typing import Any, Optional
def _f(v: Any) -> Optional[float]:
if v is None or v == "":
return None
try:
return float(v)
except (TypeError, ValueError):
return None
def perp_coin_amount(*, contracts: float, contract_size: float) -> float:
return float(contracts) * float(contract_size or 1.0)
def perp_pnl(
*,
direction: str,
entry: float,
exit_px: float,
contracts: float,
contract_size: float,
) -> float:
coins = perp_coin_amount(contracts=contracts, contract_size=contract_size)
d = (direction or "long").strip().lower()
if d == "short":
return (float(entry) - float(exit_px)) * coins
return (float(exit_px) - float(entry)) * coins
def option_premium_total(*, ask: float, sheets: float, ct_mult: float) -> float:
"""卖一报价为每 1 币;权利金 = ask × 张数 × ct_mult."""
return float(ask) * float(sheets) * float(ct_mult or 0.01)
def option_expiry_pnl(
*,
opt_type: str,
strike: float,
spot: float,
sheets: float,
ct_mult: float,
premium_paid: float,
) -> float:
o = (opt_type or "").strip().upper()
intrinsic_per_coin = 0.0
if o in ("C", "CALL"):
intrinsic_per_coin = max(0.0, float(spot) - float(strike))
elif o in ("P", "PUT"):
intrinsic_per_coin = max(0.0, float(strike) - float(spot))
else:
return -float(premium_paid)
value = intrinsic_per_coin * float(sheets) * float(ct_mult or 0.01)
return value - float(premium_paid)
def suggest_contracts_from_notional(
*,
notional: float,
entry: float,
contract_size: float,
) -> float:
if entry <= 0 or contract_size <= 0 or notional <= 0:
return 0.0
return float(notional) / (float(entry) * float(contract_size))
def build_perp_options_preview(
*,
direction: str,
entry: float,
tp: float,
sl: float,
contracts: float,
contract_size: float,
opt_type: str,
strike: float,
sheets: float,
ct_mult: float,
premium_paid: float,
index_px: Optional[float] = None,
) -> dict[str, Any]:
"""
永期情景.
止盈账:永续止盈盈利 - 权利金.
止损账:期权到期内在(按 SL 价) - 永续止损亏损额.
"""
d = (direction or "long").strip().lower()
pnl_tp_perp = perp_pnl(
direction=d, entry=entry, exit_px=tp, contracts=contracts, contract_size=contract_size
)
pnl_sl_perp = perp_pnl(
direction=d, entry=entry, exit_px=sl, contracts=contracts, contract_size=contract_size
)
# 止盈统计口径
tp_total = float(pnl_tp_perp) - float(premium_paid)
# 止损:期权按 SL 价结算内在 - |永续亏损|
opt_at_sl = option_expiry_pnl(
opt_type=opt_type,
strike=strike,
spot=sl,
sheets=sheets,
ct_mult=ct_mult,
premium_paid=premium_paid,
)
sl_total = float(opt_at_sl) - abs(float(pnl_sl_perp)) if pnl_sl_perp < 0 else float(opt_at_sl) + float(
pnl_sl_perp
)
# 有符号相加更稳:期权盈亏 + 永续盈亏
sl_total_signed = float(opt_at_sl) + float(pnl_sl_perp)
spot = float(index_px) if index_px is not None else float(entry)
opt_flat = option_expiry_pnl(
opt_type=opt_type,
strike=strike,
spot=spot,
sheets=sheets,
ct_mult=ct_mult,
premium_paid=premium_paid,
)
flat_total = 0.0 + float(opt_flat)
opt_at_tp = option_expiry_pnl(
opt_type=opt_type,
strike=strike,
spot=tp,
sheets=sheets,
ct_mult=ct_mult,
premium_paid=premium_paid,
)
return {
"plan_type": "perp_options",
"direction": d,
"contracts": contracts,
"coin_amount": perp_coin_amount(contracts=contracts, contract_size=contract_size),
"premium_paid": round(float(premium_paid), 6),
"scenarios": [
{
"id": "tp",
"label": "止盈(计划结束口径)",
"spot": tp,
"perp_pnl": round(pnl_tp_perp, 4),
"options_pnl": round(-float(premium_paid), 4),
"total": round(tp_total, 4),
"note": "止盈盈利 权利金;期权可不强平",
},
{
"id": "sl",
"label": "止损(计划结束口径)",
"spot": sl,
"perp_pnl": round(pnl_sl_perp, 4),
"options_pnl": round(opt_at_sl, 4),
"total": round(sl_total_signed, 4),
"note": "期权盈利 − 永续亏损(有符号相加);期权须强平",
},
{
"id": "flat",
"label": "到期·现价附近",
"spot": spot,
"perp_pnl": 0.0,
"options_pnl": round(opt_flat, 4),
"total": round(flat_total, 4),
"note": "示意:永续未动,期权按到期内在",
},
{
"id": "expiry_tp",
"label": "到期·止盈价",
"spot": tp,
"perp_pnl": round(pnl_tp_perp, 4),
"options_pnl": round(opt_at_tp, 4),
"total": round(pnl_tp_perp + opt_at_tp, 4),
"note": "若期权拿到 TP 价到期(参考)",
},
{
"id": "expiry_sl",
"label": "到期·止损价",
"spot": sl,
"perp_pnl": round(pnl_sl_perp, 4),
"options_pnl": round(opt_at_sl, 4),
"total": round(pnl_sl_perp + opt_at_sl, 4),
"note": "与止损口径相近(期权用内在)",
},
],
"summary": {
"tp_total": round(tp_total, 4),
"sl_total": round(sl_total_signed, 4),
"premium_paid": round(float(premium_paid), 4),
"hedge_ratio_at_sl": _hedge_ratio(opt_at_sl, pnl_sl_perp),
},
}
def _hedge_ratio(opt_pnl: float, perp_pnl: float) -> Optional[float]:
loss = abs(float(perp_pnl)) if float(perp_pnl) < 0 else 0.0
if loss <= 1e-12:
return None
if float(opt_pnl) <= 0:
return 0.0
return round(float(opt_pnl) / loss * 100.0, 2)
def build_options_options_preview(
*,
target_price: float,
index_px: float,
leg_a: dict[str, Any],
leg_b: dict[str, Any],
) -> dict[str, Any]:
"""期期情景:目标价 / 到期现价 / 到期两边."""
def _leg_pnl(leg: dict[str, Any], spot: float) -> float:
return option_expiry_pnl(
opt_type=str(leg.get("opt_type") or ""),
strike=float(leg["strike"]),
spot=spot,
sheets=float(leg.get("sheets") or 0),
ct_mult=float(leg.get("ct_mult") or 0.01),
premium_paid=float(leg.get("premium_paid") or 0),
)
prem = float(leg_a.get("premium_paid") or 0) + float(leg_b.get("premium_paid") or 0)
a_t = _leg_pnl(leg_a, target_price)
b_t = _leg_pnl(leg_b, target_price)
at_target = a_t + b_t
win_leg = "a" if a_t >= b_t else "b"
a_flat = _leg_pnl(leg_a, index_px)
b_flat = _leg_pnl(leg_b, index_px)
flat_total = a_flat + b_flat
expiry_loss = flat_total if flat_total <= 0 else flat_total
return {
"plan_type": "options_options",
"premium_paid": round(prem, 6),
"target_price": target_price,
"winner_at_target": win_leg,
"scenarios": [
{
"id": "target",
"label": "到达目标价",
"spot": target_price,
"leg_a_pnl": round(a_t, 4),
"leg_b_pnl": round(b_t, 4),
"total": round(at_target, 4),
"note": f"盈利方≈腿{win_leg.upper()}(可平);亏损方默认到期",
},
{
"id": "expiry_flat",
"label": "到期·现价(无突破)",
"spot": index_px,
"leg_a_pnl": round(a_flat, 4),
"leg_b_pnl": round(b_flat, 4),
"total": round(flat_total, 4),
"note": "无盈利则记总亏损结束" if flat_total <= 0 else "到期仍可能有净值",
},
{
"id": "max_premium_loss",
"label": "最大保费损耗",
"spot": None,
"leg_a_pnl": round(-float(leg_a.get("premium_paid") or 0), 4),
"leg_b_pnl": round(-float(leg_b.get("premium_paid") or 0), 4),
"total": round(-prem, 4),
"note": "双腿权利金全部损失",
},
],
"summary": {
"at_target_total": round(at_target, 4),
"expiry_flat_total": round(expiry_loss, 4),
"premium_paid": round(prem, 4),
"expiry_is_loss": flat_total <= 0,
},
}
def gate_status(
*,
hedge_enabled: bool,
sizing_mode: str,
plan_type: str,
options_enabled: bool,
) -> dict[str, Any]:
from lib.trade.position_sizing_lib import is_full_margin_mode
full = is_full_margin_mode(sizing_mode)
pt = (plan_type or "").strip().lower()
can_preview = True
can_start = False
reasons: list[str] = []
if not hedge_enabled:
can_start = False
reasons.append("对冲计划未启用(HEDGE_PLAN_ENABLED)")
if not options_enabled:
can_preview = False
can_start = False
reasons.append("期权模块未启用")
if pt == "perp_options":
if not full:
can_start = False
reasons.append("永期开仓仅全仓模式可用(当前可测算)")
elif hedge_enabled and options_enabled:
can_start = False
reasons.append("P0 仅测算,真实开仓将在后续版本开放")
elif pt == "options_options":
if hedge_enabled and options_enabled:
can_start = False
reasons.append("P0 仅测算,真实开仓将在后续版本开放")
return {
"hedge_enabled": hedge_enabled,
"options_enabled": options_enabled,
"sizing_mode": sizing_mode,
"is_full_margin": full,
"plan_type": pt,
"can_preview": can_preview,
"can_start": can_start,
"reasons": reasons,
}
+327
View File
@@ -0,0 +1,327 @@
"""OKX 对冲计划:P0 测算页与 API 注册."""
from __future__ import annotations
import os
from typing import Any
from flask import Flask, jsonify, request
from jinja2 import ChoiceLoader, FileSystemLoader
from lib.hedge_plan.hedge_plan_calc_lib import (
build_options_options_preview,
build_perp_options_preview,
gate_status,
option_premium_total,
suggest_contracts_from_notional,
)
from lib.trade.position_sizing_lib import (
compute_full_margin_sizing,
load_position_sizing_mode,
)
def _env_bool(key: str, default: bool = False) -> bool:
raw = (os.getenv(key) or "").strip().lower()
if not raw:
return default
return raw in ("1", "true", "yes", "on")
def attach_hedge_plan_templates(app: Flask, repo_root: str) -> None:
tpl_dir = os.path.join(repo_root, "lib", "hedge_plan", "templates")
if not os.path.isdir(tpl_dir):
return
existing = app.jinja_loader
loaders = [FileSystemLoader(tpl_dir)]
if existing is not None:
if isinstance(existing, ChoiceLoader):
loaders = list(existing.loaders) + loaders
else:
loaders.insert(0, existing)
app.jinja_loader = ChoiceLoader(loaders)
def install_hedge_plan(app: Flask, repo_root: str, app_module: Any) -> None:
attach_hedge_plan_templates(app, repo_root)
cfg = _build_cfg(app_module)
app.extensions["hedge_plan_cfg"] = cfg
register_hedge_plan_routes(app, cfg)
def _build_cfg(app_module: Any) -> dict[str, Any]:
from lib.exchange.okx_options_lib import build_option_chain
return {
"get_db": app_module.get_db,
"login_required": app_module.login_required,
"render_main_page": app_module.render_main_page,
"exchange": getattr(app_module, "exchange", None),
"exchange_options": getattr(app_module, "exchange_options", None),
"get_available_trading_usdt": getattr(app_module, "get_available_trading_usdt", None),
"get_contract_size": getattr(app_module, "get_contract_size", None),
"normalize_exchange_symbol": getattr(app_module, "normalize_exchange_symbol", None),
"ensure_markets_loaded": getattr(app_module, "ensure_markets_loaded", None),
"build_option_chain": build_option_chain,
"btc_leverage": int(getattr(app_module, "BTC_LEVERAGE", 10) or 10),
"alt_leverage": int(getattr(app_module, "ALT_LEVERAGE", 5) or 5),
"full_margin_buffer": float(getattr(app_module, "FULL_MARGIN_BUFFER_RATIO", 0.98) or 0.98),
"funds_decimals": int(getattr(app_module, "FUNDS_DECIMALS", 2) or 2),
"options_enabled": _env_bool("OKX_OPTIONS_ENABLED", False),
"default_underly": (os.getenv("OKX_OPTIONS_DEFAULT_UNDERLY") or "ETH").strip().upper(),
"chain_max_dte": float(os.getenv("OKX_OPTIONS_CHAIN_MAX_DTE_DAYS") or os.getenv("OKX_OPTIONS_MAX_DTE_DAYS") or "14"),
}
def _hedge_enabled() -> bool:
return _env_bool("HEDGE_PLAN_ENABLED", False)
def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None:
lr = cfg["login_required"]
@app.route("/hedge-plan")
@lr
def page_hedge_plan():
from lib.instance.instance_embed_lib import redirect_to_embed_shell_if_enabled
redir = redirect_to_embed_shell_if_enabled("hedge_plan")
if redir is not None:
return redir
return cfg["render_main_page"]("hedge_plan")
@app.route("/api/hedge-plan/gates")
@lr
def api_hedge_gates():
plan_type = (request.args.get("plan_type") or "perp_options").strip()
return jsonify(
{
"ok": True,
**gate_status(
hedge_enabled=_hedge_enabled(),
sizing_mode=load_position_sizing_mode(),
plan_type=plan_type,
options_enabled=bool(cfg.get("options_enabled")),
),
}
)
@app.route("/api/hedge-plan/market")
@lr
def api_hedge_market():
base = (request.args.get("base") or cfg.get("default_underly") or "ETH").strip().upper()
if base not in ("BTC", "ETH"):
return jsonify({"ok": False, "msg": "对冲计划仅支持 BTC/ETH"}), 400
direction = (request.args.get("direction") or "long").strip().lower()
if direction not in ("long", "short"):
direction = "long"
data, err = _fetch_perp_market(cfg, base)
if err:
return jsonify({"ok": False, "msg": err}), 400
sizing_mode = load_position_sizing_mode()
gates = gate_status(
hedge_enabled=_hedge_enabled(),
sizing_mode=sizing_mode,
plan_type="perp_options",
options_enabled=bool(cfg.get("options_enabled")),
)
out = {
"ok": True,
"base": base,
"direction": direction,
"suggested_opt_type": "P" if direction == "long" else "C",
**data,
"gates": gates,
"sizing_mode": sizing_mode,
}
return jsonify(out)
@app.route("/api/hedge-plan/options-chain")
@lr
def api_hedge_options_chain():
if not cfg.get("options_enabled"):
return jsonify({"ok": False, "msg": "期权模块未启用"}), 400
ex = cfg.get("exchange_options")
if ex is None:
return jsonify({"ok": False, "msg": "期权交易所未初始化"}), 400
u = (request.args.get("underlying") or cfg.get("default_underly") or "ETH").upper()
try:
chain = cfg["build_option_chain"](
ex,
u,
max_dte_days=float(cfg.get("chain_max_dte") or 14),
itm_only=False,
itm_max_dist_usd=float(os.getenv("OKX_OPTIONS_ITM_MAX_DIST_USD") or "30"),
)
except Exception as e:
return jsonify({"ok": False, "msg": f"拉取期权链失败: {e}"}), 500
return jsonify({"ok": True, **chain, "chain_max_dte_days": cfg.get("chain_max_dte")})
@app.route("/api/hedge-plan/preview", methods=["POST"])
@lr
def api_hedge_preview():
body = request.get_json(silent=True) or {}
plan_type = (body.get("plan_type") or "perp_options").strip().lower()
gates = gate_status(
hedge_enabled=_hedge_enabled(),
sizing_mode=load_position_sizing_mode(),
plan_type=plan_type,
options_enabled=bool(cfg.get("options_enabled")),
)
if not gates.get("can_preview"):
return jsonify({"ok": False, "msg": "; ".join(gates.get("reasons") or ["不可测算"]), "gates": gates}), 400
try:
if plan_type == "options_options":
data = _preview_oo(body)
else:
data = _preview_po(body)
except ValueError as e:
return jsonify({"ok": False, "msg": str(e)}), 400
except Exception as e:
return jsonify({"ok": False, "msg": f"测算失败: {e}"}), 500
return jsonify({"ok": True, "gates": gates, **data})
def _preview_po(body: dict[str, Any]) -> dict[str, Any]:
direction = str(body.get("direction") or "long").lower()
entry = float(body["entry"])
tp = float(body["tp"])
sl = float(body["sl"])
contracts = float(body["contracts"])
contract_size = float(body.get("contract_size") or 0.01)
opt_type = str(body.get("opt_type") or ("P" if direction == "long" else "C"))
strike = float(body["strike"])
sheets = float(body.get("sheets") or 1)
ct_mult = float(body.get("ct_mult") or 0.01)
ask = body.get("ask")
premium = body.get("premium_paid")
if premium is None:
if ask is None:
raise ValueError("缺少权利金或卖一价")
premium = option_premium_total(ask=float(ask), sheets=sheets, ct_mult=ct_mult)
index_px = body.get("index_px")
return build_perp_options_preview(
direction=direction,
entry=entry,
tp=tp,
sl=sl,
contracts=contracts,
contract_size=contract_size,
opt_type=opt_type,
strike=strike,
sheets=sheets,
ct_mult=ct_mult,
premium_paid=float(premium),
index_px=float(index_px) if index_px is not None else None,
)
def _preview_oo(body: dict[str, Any]) -> dict[str, Any]:
target = float(body["target_price"])
index_px = float(body.get("index_px") or target)
leg_a = body.get("leg_a") or {}
leg_b = body.get("leg_b") or {}
for name, leg in (("leg_a", leg_a), ("leg_b", leg_b)):
if not leg.get("strike"):
raise ValueError(f"缺少 {name} 行权价")
if leg.get("premium_paid") is None and leg.get("ask") is not None:
leg["premium_paid"] = option_premium_total(
ask=float(leg["ask"]),
sheets=float(leg.get("sheets") or 1),
ct_mult=float(leg.get("ct_mult") or 0.01),
)
if leg.get("premium_paid") is None:
raise ValueError(f"缺少 {name} 权利金")
return build_options_options_preview(
target_price=target,
index_px=index_px,
leg_a=leg_a,
leg_b=leg_b,
)
def _fetch_perp_market(cfg: dict[str, Any], base: str) -> tuple[dict[str, Any], str | None]:
ex = cfg.get("exchange")
if ex is None:
return {}, "永续交易所未初始化"
ensure = cfg.get("ensure_markets_loaded")
if callable(ensure):
try:
ensure()
except Exception as e:
return {}, f"加载市场失败: {e}"
norm = cfg.get("normalize_exchange_symbol")
sym = f"{base}/USDT:USDT"
if callable(norm):
try:
sym = norm(f"{base}/USDT")
except Exception:
sym = f"{base}/USDT:USDT"
mark = bid = ask = last = None
try:
t = ex.fetch_ticker(sym)
last = _sf(t.get("last"))
mark = _sf(t.get("info", {}).get("markPx")) if isinstance(t.get("info"), dict) else None
if mark is None:
mark = _sf(t.get("mark")) or last
bid = _sf(t.get("bid"))
ask = _sf(t.get("ask"))
except Exception as e:
return {}, f"拉永续行情失败: {e}"
cs = 0.01
get_cs = cfg.get("get_contract_size")
if callable(get_cs):
try:
cs = float(get_cs(sym) or 0.01)
except Exception:
cs = 0.01
available = None
get_av = cfg.get("get_available_trading_usdt")
if callable(get_av):
try:
available = get_av()
except Exception:
available = None
entry = float(mark or last or 0)
sizing = None
suggest_contracts = None
if available is not None and entry > 0:
sizing, _serr = compute_full_margin_sizing(
symbol=sym,
available_usdt=float(available),
capital_base=float(available),
buffer_ratio=float(cfg.get("full_margin_buffer") or 0.98),
btc_leverage=int(cfg.get("btc_leverage") or 10),
alt_leverage=int(cfg.get("alt_leverage") or 5),
funds_decimals=int(cfg.get("funds_decimals") or 2),
)
if sizing:
suggest_contracts = suggest_contracts_from_notional(
notional=float(sizing["notional_value"]),
entry=entry,
contract_size=cs,
)
return {
"exchange_symbol": sym,
"mark": mark,
"last": last,
"bid": bid,
"ask": ask,
"contract_size": cs,
"available_usdt": available,
"full_margin_sizing": sizing,
"suggest_contracts": round(suggest_contracts, 6) if suggest_contracts is not None else None,
"entry_ref": entry or None,
}, None
def _sf(v: Any) -> float | None:
if v is None or v == "":
return None
try:
return float(v)
except (TypeError, ValueError):
return None
@@ -0,0 +1,144 @@
<div class="hedge-plan-page-wrap" style="grid-column:1/-1" id="hedge-plan-root"
data-default-underly="{{ options_default_underly | default('ETH') }}"
data-hedge-enabled="{{ '1' if hedge_plan_enabled else '0' }}"
data-options-enabled="{{ '1' if options_enabled else '0' }}"
data-sizing-mode="{{ position_sizing_mode | default('risk') }}"
data-is-full-margin="{{ '1' if position_sizing_mode == 'full_margin' else '0' }}">
{% if not hedge_plan_enabled %}
<div class="flash" style="margin-bottom:12px">对冲计划未启用:请在 <code>env配置 → 对冲计划</code> 打开 <code>HEDGE_PLAN_ENABLED</code>(可热更).</div>
{% endif %}
{% if not options_enabled %}
<div class="flash" style="margin-bottom:12px">期权模块未启用,无法拉期权链.请先配置期权账户.</div>
{% endif %}
<div class="card" style="margin-bottom:12px">
<div style="display:flex;flex-wrap:wrap;gap:8px;align-items:center;justify-content:space-between">
<h2 style="margin:0">对冲计划 <span class="muted" style="font-size:.85rem;font-weight:400">P0 测算</span></h2>
<div class="form-row" style="margin:0">
<button type="button" class="btn-secondary hp-mode-btn active" data-mode="perp_options">永期对冲</button>
<button type="button" class="btn-secondary hp-mode-btn" data-mode="options_options">期期对冲</button>
<button type="button" class="btn-secondary" id="hp-refresh">刷新行情</button>
</div>
</div>
<p class="muted" id="hp-gate-line" style="margin:8px 0 0"></p>
</div>
<div class="options-dual-grid" id="hp-po-layout">
<div class="card">
<h2>永续(列表行情)</h2>
<div class="form-row">
<button type="button" class="btn-secondary hp-uly-btn active" data-uly="ETH">ETH</button>
<button type="button" class="btn-secondary hp-uly-btn" data-uly="BTC">BTC</button>
<select id="hp-direction">
<option value="long">做多</option>
<option value="short">做空</option>
</select>
</div>
<div id="hp-perp-quote" class="muted" style="margin:8px 0;line-height:1.6">加载中…</div>
<div class="form-row" style="flex-wrap:wrap">
<label>开仓价 <input type="number" step="any" id="hp-entry" /></label>
<label>止盈 <input type="number" step="any" id="hp-tp" /></label>
<label>止损 <input type="number" step="any" id="hp-sl" /></label>
<label>张数 <input type="number" step="any" id="hp-contracts" /></label>
</div>
<p class="muted" id="hp-sizing-line"></p>
</div>
<div class="card">
<h2>期权(列表) · <span id="hp-opt-type-label">Put</span></h2>
<div class="form-row">
<select id="hp-exp-select"><option value="">选择到期日</option></select>
<button type="button" class="btn-secondary" id="hp-load-chain">刷新链</button>
</div>
<div id="hp-index-line" class="muted"></div>
<div class="options-strike-table-wrap">
<table class="options-strike-table" id="hp-strike-table">
<thead>
<tr>
<th>行权价</th>
<th>类型</th>
<th>卖一/张</th>
<th>买一</th>
<th>操作</th>
</tr>
</thead>
<tbody id="hp-strike-tbody">
<tr><td colspan="5" class="muted">请刷新期权链</td></tr>
</tbody>
</table>
</div>
<div class="form-row" style="margin-top:8px;flex-wrap:wrap">
<label>已选合约 <code id="hp-sel-inst"></code></label>
<label>张数 <input type="number" step="1" min="1" id="hp-sheets" value="1" /></label>
<span class="muted" id="hp-premium-line"></span>
</div>
</div>
</div>
<div class="options-dual-grid hidden" id="hp-oo-layout">
<div class="card">
<h2>期期参数</h2>
<div class="form-row">
<button type="button" class="btn-secondary hp-uly-btn-oo active" data-uly="ETH">ETH</button>
<button type="button" class="btn-secondary hp-uly-btn-oo" data-uly="BTC">BTC</button>
<label>目标价 S* <input type="number" step="any" id="hp-target" /></label>
</div>
<div id="hp-oo-index" class="muted"></div>
<div id="hp-oo-legs" class="muted" style="margin-top:8px;line-height:1.6">尚未选用两腿</div>
</div>
<div class="card">
<h2>期权 T 型报价</h2>
<div class="form-row">
<select id="hp-oo-exp-select"><option value="">选择到期日</option></select>
<button type="button" class="btn-secondary" id="hp-oo-load-chain">刷新链</button>
</div>
<div class="options-strike-table-wrap options-strike-table-wrap--t">
<table class="options-strike-table options-strike-table--t" id="hp-oo-table">
<thead>
<tr>
<th colspan="2" class="opt-t-head-call">Call</th>
<th class="opt-t-head-mid">行权</th>
<th colspan="2" class="opt-t-head-put">Put</th>
</tr>
<tr>
<th>卖一</th><th>选用</th>
<th>K</th>
<th>卖一</th><th>选用</th>
</tr>
</thead>
<tbody id="hp-oo-tbody">
<tr><td colspan="5" class="muted">请刷新期权链</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="card" style="margin-top:12px">
<div style="display:flex;flex-wrap:wrap;gap:8px;align-items:center;justify-content:space-between">
<h2 style="margin:0">情景测算</h2>
<div class="form-row" style="margin:0">
<button type="button" class="primary" id="hp-preview-btn">计算</button>
<button type="button" class="btn-secondary" id="hp-start-btn" disabled title="P0 不开仓">启动计划(P0禁用)</button>
</div>
</div>
<div id="hp-summary" class="muted" style="margin:8px 0"></div>
<div class="options-strike-table-wrap">
<table class="options-strike-table" id="hp-result-table">
<thead>
<tr>
<th>情景</th>
<th>现货价</th>
<th>永续/腿盈亏</th>
<th>期权盈亏</th>
<th>合计≈U</th>
<th>说明</th>
</tr>
</thead>
<tbody id="hp-result-tbody">
<tr><td colspan="6" class="muted">填写参数后点计算</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<script src="/static/hedge_plan.js?v=1"></script>