diff --git a/lib/common/static/hedge_plan.js b/lib/common/static/hedge_plan.js
index 2142438..c3cb4cd 100644
--- a/lib/common/static/hedge_plan.js
+++ b/lib/common/static/hedge_plan.js
@@ -726,7 +726,7 @@
el.textContent = "持仓中" + idPart;
el.classList.add("is-holding");
} else {
- el.textContent = "未启动";
+ el.textContent = "";
el.classList.add("is-idle");
}
}
@@ -990,6 +990,10 @@
"标记 " +
fmt(opt.mark_px, 4) +
"" +
+ '权利金 ' +
+ (opt.premium_coin != null ? fmt(opt.premium_coin, 4) + " 币" : "—") +
+ (opt.premium_usdt != null ? " / " + fmt(opt.premium_usdt, 2) + " U" : "") +
+ "" +
' 0 ? prem * buyPx : null;
+ line.textContent =
+ "权利金 ≈ " +
+ fmt(prem, 4) +
+ " 币" +
+ (premU != null ? " / " + fmt(premU, 2) + " U" : "") +
+ " · 卖一 " +
+ fmt(ask, 4) +
+ (buyPx > 0 ? " · 买币参考 " + fmt(buyPx, 2) : "");
}
function buildStraddleRows(contracts) {
diff --git a/lib/common/static/instance_theme.css b/lib/common/static/instance_theme.css
index dbef1a8..7a135e7 100644
--- a/lib/common/static/instance_theme.css
+++ b/lib/common/static/instance_theme.css
@@ -4246,6 +4246,14 @@ html[data-theme="light"] .opt-be-dist-down {
color: #e8ecf4;
font-weight: 600;
}
+.hedge-plan-page-wrap .hp-live-leg-grid strong.hp-pnl-pos,
+.hedge-plan-page-wrap .hp-live-combo strong.hp-pnl-pos {
+ color: #7ee787 !important;
+}
+.hedge-plan-page-wrap .hp-live-leg-grid strong.hp-pnl-neg,
+.hedge-plan-page-wrap .hp-live-combo strong.hp-pnl-neg {
+ color: #ff8a8a !important;
+}
.hedge-plan-page-wrap .hp-live-combo {
display: flex;
justify-content: space-between;
diff --git a/lib/exchange/okx_options_lib.py b/lib/exchange/okx_options_lib.py
index 826b297..6dd1186 100644
--- a/lib/exchange/okx_options_lib.py
+++ b/lib/exchange/okx_options_lib.py
@@ -706,11 +706,17 @@ def options_header_balance_pack(
margin_mode = "usdc"
underly = (os.getenv("OKX_OPTIONS_DEFAULT_UNDERLY") or "ETH").strip().upper() or "ETH"
coin_key = "btc" if underly == "BTC" else "eth"
+ trading_usdt = _round(bal.get("trading_usdt"))
+ trading_usdt_avail = _round(bal.get("trading_usdt_avail"))
+ if trading_usdt_avail is None:
+ trading_usdt_avail = trading_usdt
return {
"trading_usdc": _round(bal.get("trading_usdc")),
"funding_usdc": _round(bal.get("funding_usdc")),
"funding_usdt": _round(bal.get("funding_usdt")),
- "trading_usdt": _round(bal.get("trading_usdt")),
+ "trading_usdt": trading_usdt,
+ "trading_usdt_avail": trading_usdt_avail,
+ "perp_margin_usdt": _round(bal.get("perp_margin_usdt")),
"funding_eth": _round_coin(bal.get("funding_eth")),
"trading_eth": _round_coin(bal.get("trading_eth")),
"funding_btc": _round_coin(bal.get("funding_btc")),
diff --git a/lib/hedge_plan/hedge_plan_live_positions_lib.py b/lib/hedge_plan/hedge_plan_live_positions_lib.py
index 5a56b0d..b098632 100644
--- a/lib/hedge_plan/hedge_plan_live_positions_lib.py
+++ b/lib/hedge_plan/hedge_plan_live_positions_lib.py
@@ -4,9 +4,10 @@ from __future__ import annotations
from datetime import datetime, timedelta, timezone
from typing import Any, Optional
-from lib.hedge_plan.hedge_plan_option_primary_lib import (
+ from lib.hedge_plan.hedge_plan_option_primary_lib import (
estimate_combo_net_pnl,
fee_rate,
+ option_fee_rate,
opt_type_for_view,
option_bid_liquidity_ok,
perp_direction_for_view,
@@ -129,6 +130,7 @@ def build_live_hedge_cards(cfg: dict[str, Any], conn: Any) -> list[dict[str, Any
get_cs = cfg.get("get_contract_size")
fetch_idx = cfg.get("fetch_index_price")
fr = fee_rate()
+ fr_opt = option_fee_rate()
now_ms = int(datetime.now(tz=_BJ).timestamp() * 1000)
out: list[dict[str, Any]] = []
@@ -226,11 +228,15 @@ def build_live_hedge_cards(cfg: dict[str, Any], conn: Any) -> list[dict[str, Any
contracts=float(contracts or 0),
contract_size=float(cs or 0.01),
fee=fr,
+ option_fee=fr_opt,
)
opt_net_coin = float(net.get("opt_net") or 0)
+ # 币→U:用当前指数(与买币参考价同口径)
opt_net_usdt = round(opt_net_coin * float(idx or 0), 4) if idx else None
perp_net = float(net.get("perp_net") or 0)
combo_usdt = round((opt_net_usdt or 0) + perp_net, 4) if opt_net_usdt is not None else round(perp_net, 4)
+ prem_coin = float(net.get("premium") or (float(avg_open or 0) * float(sheets or 0) * float(ct or 0.01)))
+ prem_usdt = round(prem_coin * float(idx or 0), 2) if idx else None
opened_at = plan.get("opened_at")
opened_ms = _parse_opened_ms(opened_at)
@@ -253,6 +259,7 @@ def build_live_hedge_cards(cfg: dict[str, Any], conn: Any) -> list[dict[str, Any
"remain_sec": remain_sec,
"index_px": idx,
"fee_rate": fr,
+ "option_fee_rate": fr_opt,
"option": {
"inst_id": inst_id or "—",
"opt_type": opt_type,
@@ -266,6 +273,8 @@ def build_live_hedge_cards(cfg: dict[str, Any], conn: Any) -> list[dict[str, Any
"leverage": opt_leverage,
"liq_ok": bool(liq_ok),
"liq_msg": liq_msg or "",
+ "premium_coin": round(prem_coin, 4),
+ "premium_usdt": prem_usdt,
"net_coin": opt_net_coin,
"net_usdt": opt_net_usdt,
"live": bool(opt_raw),
diff --git a/lib/hedge_plan/hedge_plan_option_primary_lib.py b/lib/hedge_plan/hedge_plan_option_primary_lib.py
index 0de83f0..d131680 100644
--- a/lib/hedge_plan/hedge_plan_option_primary_lib.py
+++ b/lib/hedge_plan/hedge_plan_option_primary_lib.py
@@ -38,12 +38,29 @@ def is_option_primary(body_or_plan: dict[str, Any] | None) -> bool:
def fee_rate() -> float:
+ """永续 taker 费率(默认 0.05%)."""
try:
return max(0.0, float(os.getenv("HEDGE_PLAN_FEE_RATE") or os.getenv("OKX_TAKER_FEE") or "0.0005"))
except (TypeError, ValueError):
return 0.0005
+def option_fee_rate() -> float:
+ """币本位期权 taker 费率(OKX 普通档默认 0.03%)."""
+ try:
+ return max(
+ 0.0,
+ float(
+ os.getenv("HEDGE_PLAN_OPTION_FEE_RATE")
+ or os.getenv("OKX_OPTIONS_TAKER_FEE")
+ or os.getenv("SIM_OPTION_FEE_RATE")
+ or "0.0003"
+ ),
+ )
+ except (TypeError, ValueError):
+ return 0.0003
+
+
def floor2(v: float) -> float:
"""ETH 数量向下取两位小数."""
if v <= 0:
@@ -201,17 +218,30 @@ def estimate_combo_net_pnl(
contracts: float,
contract_size: float,
fee: Optional[float] = None,
+ option_fee: Optional[float] = None,
) -> dict[str, Any]:
- """组合净利(扣费);平仓/卖出手续费按买入费率估算."""
- fr = fee if fee is not None else fee_rate()
+ """组合净利(扣开平双边手续费).
+
+ 期权:吃卖一开/买一平(无滑点);费=Min(费率×标的币, 权利金×7%).
+ 永续:名义×费率(开+平).
+ """
+ from lib.sim.pricing_lib import option_fee_coin
+
+ fr_perp = fee if fee is not None else fee_rate()
+ fr_opt = option_fee if option_fee is not None else option_fee_rate()
ct = float(ct_mult or 0.01)
sh = float(sheets or 0)
a = float(ask_open or 0)
b = float(bid or 0)
- premium = a * sh * ct
- opt_proceeds = b * sh * ct
- opt_open_fee = premium * fr
- opt_close_fee = opt_proceeds * fr # 卖出费用按买入费率
+ underlying = sh * ct
+ premium = a * underlying
+ opt_proceeds = b * underlying
+ opt_open_fee = option_fee_coin(
+ underlying_coin=underlying, premium_coin=premium, fee_rate=fr_opt
+ )
+ opt_close_fee = option_fee_coin(
+ underlying_coin=underlying, premium_coin=opt_proceeds, fee_rate=fr_opt
+ )
opt_net = opt_proceeds - premium - opt_open_fee - opt_close_fee
coins = float(contracts or 0) * float(contract_size or 0.01)
@@ -224,15 +254,20 @@ def estimate_combo_net_pnl(
perp_gross = (mark - entry) * coins
perp_notional_open = abs(entry * coins)
perp_notional_close = abs(mark * coins)
- perp_open_fee = perp_notional_open * fr
- perp_close_fee = perp_notional_close * fr
+ perp_open_fee = perp_notional_open * fr_perp
+ perp_close_fee = perp_notional_close * fr_perp
perp_net = perp_gross - perp_open_fee - perp_close_fee
total = opt_net + perp_net
return {
"opt_net": round(opt_net, 4),
"perp_net": round(perp_net, 4),
"net": round(total, 4),
- "fee_rate": fr,
+ "fee_rate": fr_perp,
+ "option_fee_rate": fr_opt,
+ "opt_open_fee": round(opt_open_fee, 8),
+ "opt_close_fee": round(opt_close_fee, 8),
+ "perp_open_fee": round(perp_open_fee, 4),
+ "perp_close_fee": round(perp_close_fee, 4),
"premium": round(premium, 4),
"opt_proceeds": round(opt_proceeds, 4),
}
diff --git a/lib/hedge_plan/hedge_plan_register.py b/lib/hedge_plan/hedge_plan_register.py
index b4fc5a5..08aee35 100644
--- a/lib/hedge_plan/hedge_plan_register.py
+++ b/lib/hedge_plan/hedge_plan_register.py
@@ -1431,30 +1431,53 @@ def _fetch_perp_market(cfg: dict[str, Any], base: str) -> tuple[dict[str, Any],
def _options_account_snapshot(cfg: dict[str, Any]) -> dict[str, Any]:
- """期权账户资金快照(与期权页同源: exchange_options)."""
+ """期权账户资金快照(与期权页同源: exchange_options).
+
+ trading_usdt=展示权益(含永续全仓占用);trading_usdt_avail=可开仓余额.
+ """
out: dict[str, Any] = {
"label": cfg.get("options_account_label") or "期权账户",
"trading_usdc": None,
"funding_usdc": None,
"trading_usdt": None,
"funding_usdt": None,
+ "trading_usdt_avail": None,
+ "perp_margin_usdt": None,
+ "margin_note": "永续全仓 · 期权逐仓",
}
ex = cfg.get("exchange_options")
- hdr = cfg.get("options_header_balances")
- if ex is None or not callable(hdr):
+ if ex is None:
return out
try:
- trading_usdc, funding_usdc, funding_usdt, trading_usdt = hdr(ex, force=False)
+ from lib.exchange.okx_options_lib import options_header_balance_pack
+
+ pack = options_header_balance_pack(ex, force=False) or {}
out.update(
{
- "trading_usdc": trading_usdc,
- "funding_usdc": funding_usdc,
- "trading_usdt": trading_usdt,
- "funding_usdt": funding_usdt,
+ "trading_usdc": pack.get("trading_usdc"),
+ "funding_usdc": pack.get("funding_usdc"),
+ "trading_usdt": pack.get("trading_usdt"),
+ "funding_usdt": pack.get("funding_usdt"),
+ "trading_usdt_avail": pack.get("trading_usdt_avail", pack.get("trading_usdt")),
+ "perp_margin_usdt": pack.get("perp_margin_usdt"),
}
)
except Exception:
- pass
+ hdr = cfg.get("options_header_balances")
+ if callable(hdr):
+ try:
+ trading_usdc, funding_usdc, funding_usdt, trading_usdt = hdr(ex, force=False)
+ out.update(
+ {
+ "trading_usdc": trading_usdc,
+ "funding_usdc": funding_usdc,
+ "trading_usdt": trading_usdt,
+ "funding_usdt": funding_usdt,
+ "trading_usdt_avail": trading_usdt,
+ }
+ )
+ except Exception:
+ pass
return out
diff --git a/lib/hedge_plan/templates/hedge_plan_panel.html b/lib/hedge_plan/templates/hedge_plan_panel.html
index dca64af..4cd3683 100644
--- a/lib/hedge_plan/templates/hedge_plan_panel.html
+++ b/lib/hedge_plan/templates/hedge_plan_panel.html
@@ -431,4 +431,4 @@
-
+
diff --git a/lib/instance/templates/embed_shell.html b/lib/instance/templates/embed_shell.html
index d9b8351..cd9a22c 100644
--- a/lib/instance/templates/embed_shell.html
+++ b/lib/instance/templates/embed_shell.html
@@ -8,7 +8,7 @@
-
+
diff --git a/lib/instance/templates/index.html b/lib/instance/templates/index.html
index ebb8ded..4b2475a 100644
--- a/lib/instance/templates/index.html
+++ b/lib/instance/templates/index.html
@@ -19,7 +19,7 @@
{{ pwa_app_name }}
-
+
dict[str, float]:
- return self.wallets.view()
+ """资金快照.trading_usdt=全仓权益(可用+永续占用保证金);trading_usdt_free=可开仓余额."""
+ w = dict(self.wallets.view())
+ free = float(w.get("trading_usdt") or 0)
+ margin = 0.0
+ try:
+ for p in self.list_perp_positions():
+ margin += float(p.get("margin_usdt") or 0)
+ except Exception:
+ margin = 0.0
+ w["trading_usdt_free"] = free
+ w["perp_margin_usdt"] = margin
+ # 永续全仓:展示权益含占用保证金;期权逐仓权利金在币余额,不并入 USDT
+ w["trading_usdt"] = round(free + margin, 8)
+ return w
def list_perp_positions(self) -> list[dict[str, Any]]:
conn = self.get_db()
@@ -431,7 +444,7 @@ class SimBroker:
n = int(sheets)
if n < 1:
return {"ok": False, "msg": "张数至少为 1"}
- fr = sim_fee_rate(fee_rate)
+ fr = sim_option_fee_rate(fee_rate)
bid, ask, ct_mult = _option_bid_ask(exchange_options, inst_id)
if price is not None and float(price) > 0:
# 限价: 用 min(limit, ask) 作为基准卖一近似, 仍走 option_fill 滑点
@@ -540,7 +553,7 @@ class SimBroker:
close_n = min(pos_s, float(n))
if close_n <= 0:
return {"ok": False, "msg": "平仓张数无效"}
- fr = sim_fee_rate(fee_rate)
+ fr = sim_option_fee_rate(fee_rate)
qty = close_n * ct_mult
pr = option_fill(action="close", bid=bid, ask=ask, qty=qty, fee_rate=fr)
credit = pr.notional - pr.fee
diff --git a/lib/sim/hooks.py b/lib/sim/hooks.py
index 37399b1..62835eb 100644
--- a/lib/sim/hooks.py
+++ b/lib/sim/hooks.py
@@ -55,7 +55,11 @@ def apply_sim_hooks(app_module: Any) -> None:
def get_available_trading_usdt():
if is_sim_mode(app_module.get_db):
- return float(broker().balances_header()["trading_usdt"])
+ w = broker().balances_header()
+ # 开仓可用=未占用保证金的 USDT(勿用全仓权益)
+ if w.get("trading_usdt_free") is not None:
+ return float(w.get("trading_usdt_free") or 0)
+ return float(w.get("trading_usdt") or 0)
return _orig_avail()
def get_live_position_contracts(exchange_symbol, direction):
@@ -215,6 +219,7 @@ def _patch_okx_options_lib(app_module: Any) -> None:
te = float(w.get("trading_eth") or 0)
fb = float(w.get("funding_btc") or 0)
tb = float(w.get("trading_btc") or 0)
+ tu_free = float(w.get("trading_usdt_free") if w.get("trading_usdt_free") is not None else tu)
return {
"scope": "main",
"funding_usdt": fu,
@@ -227,12 +232,13 @@ def _patch_okx_options_lib(app_module: Any) -> None:
"trading_btc": tb,
"funding_usdt_avail": fu,
"funding_usdc_avail": fc,
- "trading_usdt_avail": tu,
+ "trading_usdt_avail": tu_free,
"trading_usdc_avail": tc,
"funding_eth_avail": fe,
"trading_eth_avail": te,
"funding_btc_avail": fb,
"trading_btc_avail": tb,
+ "perp_margin_usdt": float(w.get("perp_margin_usdt") or 0),
}
except Exception:
pass
diff --git a/lib/sim/pricing_lib.py b/lib/sim/pricing_lib.py
index 495ead9..57e47d1 100644
--- a/lib/sim/pricing_lib.py
+++ b/lib/sim/pricing_lib.py
@@ -1,8 +1,8 @@
"""成交价与手续费.
永续:滑点=1×fee_rate,手续费=名义×fee_rate.
-期权(币本位):滑点仍按权利金价×fee_rate;手续费对齐 OKX=
- Min(fee_rate×标的币数量, 权利金×7%).
+期权(币本位):吃卖一/买一按报价成交(无额外滑点);手续费对齐 OKX=
+ Min(fee_rate×标的币数量, 权利金×7%);普通用户 taker 默认 0.03%.
"""
from __future__ import annotations
@@ -32,6 +32,20 @@ def sim_fee_rate(override: float | None = None) -> float:
return 0.0005
+def sim_option_fee_rate(override: float | None = None) -> float:
+ """OKX 币本位期权 taker 费率(默认 0.03%)."""
+ if override is not None:
+ return float(override)
+ try:
+ return float(
+ os.getenv("SIM_OPTION_FEE_RATE")
+ or os.getenv("OKX_OPTIONS_TAKER_FEE")
+ or "0.0003"
+ )
+ except (TypeError, ValueError):
+ return 0.0003
+
+
def perp_fill(
*,
side: str,
@@ -74,7 +88,7 @@ def option_fee_coin(
Min(费率 × 张数对应标的币数量, 权利金×封顶比例).
注意:费率作用在标的币面值上,不是权利金名义上.
"""
- f = sim_fee_rate(fee_rate)
+ f = sim_option_fee_rate(fee_rate)
qty = abs(float(underlying_coin or 0.0))
prem = abs(float(premium_coin or 0.0))
if qty <= 0 and prem <= 0:
@@ -92,20 +106,19 @@ def option_fill(
bid: float,
ask: float,
qty: float,
- fee_rate: float,
+ fee_rate: float | None = None,
) -> PriceResult:
"""开仓买入吃卖一; 平仓卖出吃买一.
qty = sheets × ct_mult(标的币数量).
- 滑点仍按权利金价×费率;手续费按 OKX 币本位公式(标的币×费率,权利金7%封顶).
+ 成交价=买卖一(无额外滑点);手续费=OKX 币本位公式(标的币×费率,权利金7%封顶).
"""
- f = float(fee_rate)
+ f = sim_option_fee_rate(fee_rate)
if action == "open":
base = float(ask)
- fill = base * (1.0 + f)
else:
base = float(bid)
- fill = base * (1.0 - f)
+ fill = base
qty_f = abs(float(qty))
notional = abs(fill * qty_f)
fee = option_fee_coin(
@@ -113,8 +126,7 @@ def option_fill(
premium_coin=notional,
fee_rate=f,
)
- slip = abs(fill - base) * qty_f
- return PriceResult(base_px=base, fill_px=fill, fee=fee, slip=slip, notional=notional)
+ return PriceResult(base_px=base, fill_px=fill, fee=fee, slip=0.0, notional=notional)
@dataclass(slots=True)