Fix btc_eth live card option premium USDT conversion.

Use option (BTC) index for coin-to-U; never fall back to ETH perp mark. Also add no-server-ops Cursor rule.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-14 20:48:10 +08:00
parent e1fd77cd06
commit e8f33c3c52
4 changed files with 153 additions and 19 deletions
+19
View File
@@ -0,0 +1,19 @@
---
description: 禁止操作服务器;只允许推送仓库
alwaysApply: true
---
# 服务器操作禁令
- **禁止**对 `192.168.8.6`(及任何生产/部署服务器)做任何操作:SSH、`git pull`、`pm2`、`systemctl`、远程脚本、文件读写、重启、部署等。
- **允许**:本地改代码;在用户明确要求时 `git push` 到远程仓库。
- 部署由用户在服务器上自行完成;agent 最多提示用户可执行的命令,**不得代为执行**。
```text
# ❌ BAD — 禁止
ssh dek@192.168.8.6 "cd /opt/coinox && git pull && pm2 restart …"
# ✅ GOOD — 仅推仓(用户明确要求时)
git push origin main
# 然后口头告知用户自行在服务器 pull/restart
```
+70 -14
View File
@@ -104,9 +104,57 @@ def _has_open_leg(legs: list[dict[str, Any]]) -> bool:
return any(str(x.get("status") or "").lower() == "open" for x in legs) return any(str(x.get("status") or "").lower() == "open" for x in legs)
def resolve_option_index_px(
cfg: dict[str, Any],
option_uly: str,
*,
ex_opt: Any = None,
cross_asset: bool = False,
perp_fallback: Optional[float] = None,
) -> Optional[float]:
"""期权币→U 必须用期权标的指数;跨币禁止回落永续价(否则 BTC 权利金会×ETH).
优先公共 HTTP(单参),再 cfg.fetch_index_price(ex, uly);同币才可用永续价兜底.
"""
uly = str(option_uly or "ETH").strip().upper() or "ETH"
try:
from lib.exchange.okx_public_http_lib import fetch_index_px
px = _sf(fetch_index_px(uly))
if px is not None and px > 0:
return px
except Exception:
pass
fetch_idx = cfg.get("fetch_index_price") if isinstance(cfg, dict) else None
if callable(fetch_idx):
try:
if ex_opt is not None:
px = _sf(fetch_idx(ex_opt, uly))
else:
# 兼容已绑定 ex 的单参包装;勿把 uly 当成 ex
px = _sf(fetch_idx(uly))
if px is not None and px > 0:
return px
except TypeError:
try:
px = _sf(fetch_idx(uly))
if px is not None and px > 0:
return px
except Exception:
pass
except Exception:
pass
if not cross_asset:
fb = _sf(perp_fallback)
if fb is not None and fb > 0:
return fb
return None
def build_live_hedge_cards(cfg: dict[str, Any], conn: Any) -> list[dict[str, Any]]: def build_live_hedge_cards(cfg: dict[str, Any], conn: Any) -> list[dict[str, Any]]:
"""当前持仓:未平完的永期仓(含计划已结束但腿仍 open).""" """当前持仓:未平完的永期仓(含计划已结束但腿仍 open)."""
from lib.hedge_plan.hedge_plan_db import attach_legs_to_plans, list_plans_for_live_positions from lib.hedge_plan.hedge_plan_db import attach_legs_to_plans, list_plans_for_live_positions
from lib.hedge_plan.hedge_po_mode_lib import combo_net_usdt, get_hedge_po_mode, is_cross_asset, option_underlying
rows = list_plans_for_live_positions(conn, plan_type="perp_options", limit=40) rows = list_plans_for_live_positions(conn, plan_type="perp_options", limit=40)
if not rows: if not rows:
@@ -129,7 +177,6 @@ def build_live_hedge_cards(cfg: dict[str, Any], conn: Any) -> list[dict[str, Any
get_perp_metrics = cfg.get("get_live_position_exchange_metrics") get_perp_metrics = cfg.get("get_live_position_exchange_metrics")
get_cs = cfg.get("get_contract_size") get_cs = cfg.get("get_contract_size")
fetch_idx = cfg.get("fetch_index_price")
ex_perp = None ex_perp = None
try: try:
get_pex = cfg.get("exchange") get_pex = cfg.get("exchange")
@@ -161,7 +208,13 @@ def build_live_hedge_cards(cfg: dict[str, Any], conn: Any) -> list[dict[str, Any
continue continue
view = str(plan.get("direction") or "long").lower() view = str(plan.get("direction") or "long").lower()
uly = str(plan.get("underlying") or "ETH").upper() mode = str(plan.get("po_mode") or get_hedge_po_mode())
uly = str(
plan.get("option_underlying") or plan.get("underlying") or option_underlying(mode) or "ETH"
).upper()
cross = is_cross_asset(mode) or (
str(plan.get("perp_underlying") or "").upper() not in ("", uly)
)
inst_id = str((opt_leg or {}).get("inst_id") or "") inst_id = str((opt_leg or {}).get("inst_id") or "")
opt_raw = _pick_option_row(opt_rows, inst_id) if inst_id else None opt_raw = _pick_option_row(opt_rows, inst_id) if inst_id else None
@@ -230,14 +283,14 @@ def build_live_hedge_cards(cfg: dict[str, Any], conn: Any) -> list[dict[str, Any
perp_top_sz = _sf(book.get("top_sz")) perp_top_sz = _sf(book.get("top_sz"))
book_side = str(book.get("book_side") or "") book_side = str(book.get("book_side") or "")
idx = None # 币→U:期权标的指数(btc_eth=BTC);禁止用 ETH 永续价折算
if callable(fetch_idx): idx = resolve_option_index_px(
try: cfg,
idx = _sf(fetch_idx(uly)) uly,
except Exception: ex_opt=ex_opt,
idx = None cross_asset=cross,
if idx is None or idx <= 0: perp_fallback=(perp_vwap or perp_mark or strike),
idx = perp_vwap or perp_mark or strike or 0.0 )
net = estimate_combo_net_pnl( net = estimate_combo_net_pnl(
view_side=view, view_side=view,
@@ -256,12 +309,15 @@ def build_live_hedge_cards(cfg: dict[str, Any], conn: Any) -> list[dict[str, Any
option_fee=fr_opt, option_fee=fr_opt,
) )
opt_net_coin = float(net.get("opt_net") or 0) opt_net_coin = float(net.get("opt_net") or 0)
# 币→U:用当前指数(与买币参考价同口径) opt_net_usdt = (
opt_net_usdt = round(opt_net_coin * float(idx or 0), 4) if idx else None round(opt_net_coin * float(idx), 4) if idx is not None and idx > 0 else None
)
perp_net = float(net.get("perp_net") or 0) 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) combo_usdt = combo_net_usdt(net, option_index=float(idx or 0))
if combo_usdt is None:
combo_usdt = 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_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 prem_usdt = round(prem_coin * float(idx), 2) if idx is not None and idx > 0 else None
opened_at = plan.get("opened_at") opened_at = plan.get("opened_at")
opened_ms = _parse_opened_ms(opened_at) opened_ms = _parse_opened_ms(opened_at)
+22 -5
View File
@@ -1775,14 +1775,31 @@ def execute_manual_close_plan(cfg: dict[str, Any], conn: Any, plan_id: int) -> d
notes.append("卖币失败: " + str(sell_r.get("msg") or "")) notes.append("卖币失败: " + str(sell_r.get("msg") or ""))
total = float(opt_pnl) + float(perp_pnl) total = float(opt_pnl) + float(perp_pnl)
# 期权币本位盈亏换算到 U(粗):用指数 # 期权币本位盈亏换算到 U:用期权标的指数(跨币=BTC,禁止×ETH)
try: try:
fetch_idx = cfg.get("fetch_index_price") from lib.hedge_plan.hedge_plan_live_positions_lib import resolve_option_index_px
idx = float(fetch_idx(uly) or 0) if callable(fetch_idx) else 0.0 from lib.hedge_plan.hedge_po_mode_lib import get_hedge_po_mode, is_cross_asset, option_underlying
mode = str(plan.get("po_mode") or get_hedge_po_mode())
opt_u = str(plan.get("option_underlying") or plan.get("underlying") or option_underlying(mode) or uly).upper()
ex_idx = cfg.get("exchange_options") or cfg.get("exchange")
if callable(ex_idx):
try:
ex_idx = ex_idx()
except Exception:
ex_idx = None
idx = float(
resolve_option_index_px(
cfg,
opt_u,
ex_opt=ex_idx,
cross_asset=is_cross_asset(mode),
perp_fallback=None,
)
or 0
)
# opt_pnl 可能已是币;若为币且绝对值很小则 × 指数 # opt_pnl 可能已是币;若为币且绝对值很小则 × 指数
# resolve_option_leg_realized_pnl 对币本位通常返回币;统一展示用 preview 风格
if abs(opt_pnl) < 10 and idx > 0 and abs(opt_pnl * idx) > abs(opt_pnl): if abs(opt_pnl) < 10 and idx > 0 and abs(opt_pnl * idx) > abs(opt_pnl):
# 更可能是币单位
opt_pnl_u = opt_pnl * idx opt_pnl_u = opt_pnl * idx
else: else:
opt_pnl_u = opt_pnl opt_pnl_u = opt_pnl
+42
View File
@@ -172,6 +172,48 @@ class TestHedgePoMode(unittest.TestCase):
self.assertEqual(p["exit_mode"], "profit") self.assertEqual(p["exit_mode"], "profit")
self.assertIn("子账户", p["instance_rule"]) self.assertIn("子账户", p["instance_rule"])
def test_resolve_option_index_cross_no_perp_fallback(self):
# 跨币缺指数时不得用 ETH 永续价折 BTC 权利金(曾导致 0.014×2476≈34U)
from lib.hedge_plan.hedge_plan_live_positions_lib import resolve_option_index_px
with patch(
"lib.exchange.okx_public_http_lib.fetch_index_px",
side_effect=RuntimeError("no net"),
):
px = resolve_option_index_px(
{},
"BTC",
ex_opt=None,
cross_asset=True,
perp_fallback=2476.0,
)
self.assertIsNone(px)
with patch(
"lib.exchange.okx_public_http_lib.fetch_index_px",
return_value=91000.0,
):
px = resolve_option_index_px(
{},
"BTC",
cross_asset=True,
perp_fallback=2476.0,
)
self.assertAlmostEqual(px, 91000.0)
# 同币允许永续价兜底
with patch(
"lib.exchange.okx_public_http_lib.fetch_index_px",
side_effect=RuntimeError("no net"),
):
px = resolve_option_index_px(
{},
"ETH",
cross_asset=False,
perp_fallback=2476.0,
)
self.assertAlmostEqual(px, 2476.0)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()