From e8f33c3c52595b15ca6455bcc9d4ddf3e1afb7bc Mon Sep 17 00:00:00 2001 From: dekun Date: Wed, 14 Oct 2026 20:48:10 +0800 Subject: [PATCH] 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 --- .cursor/rules/no-server-ops.mdc | 19 +++++ .../hedge_plan_live_positions_lib.py | 84 +++++++++++++++---- lib/hedge_plan/hedge_plan_orders_lib.py | 27 ++++-- tests/test_hedge_po_mode.py | 42 ++++++++++ 4 files changed, 153 insertions(+), 19 deletions(-) create mode 100644 .cursor/rules/no-server-ops.mdc diff --git a/.cursor/rules/no-server-ops.mdc b/.cursor/rules/no-server-ops.mdc new file mode 100644 index 0000000..ca6d154 --- /dev/null +++ b/.cursor/rules/no-server-ops.mdc @@ -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 +``` diff --git a/lib/hedge_plan/hedge_plan_live_positions_lib.py b/lib/hedge_plan/hedge_plan_live_positions_lib.py index fbd85c6..2887cb7 100644 --- a/lib/hedge_plan/hedge_plan_live_positions_lib.py +++ b/lib/hedge_plan/hedge_plan_live_positions_lib.py @@ -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) +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]]: """当前持仓:未平完的永期仓(含计划已结束但腿仍 open).""" 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) 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_cs = cfg.get("get_contract_size") - fetch_idx = cfg.get("fetch_index_price") ex_perp = None try: 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 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 "") 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")) book_side = str(book.get("book_side") or "") - idx = None - if callable(fetch_idx): - try: - idx = _sf(fetch_idx(uly)) - except Exception: - idx = None - if idx is None or idx <= 0: - idx = perp_vwap or perp_mark or strike or 0.0 + # 币→U:期权标的指数(btc_eth=BTC);禁止用 ETH 永续价折算 + idx = resolve_option_index_px( + cfg, + uly, + ex_opt=ex_opt, + cross_asset=cross, + perp_fallback=(perp_vwap or perp_mark or strike), + ) net = estimate_combo_net_pnl( 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, ) 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 + opt_net_usdt = ( + 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) - 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_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_ms = _parse_opened_ms(opened_at) diff --git a/lib/hedge_plan/hedge_plan_orders_lib.py b/lib/hedge_plan/hedge_plan_orders_lib.py index e9142cb..393e51a 100644 --- a/lib/hedge_plan/hedge_plan_orders_lib.py +++ b/lib/hedge_plan/hedge_plan_orders_lib.py @@ -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 "")) total = float(opt_pnl) + float(perp_pnl) - # 期权币本位盈亏换算到 U(粗):用指数 + # 期权币本位盈亏换算到 U:用期权标的指数(跨币=BTC,禁止×ETH) try: - fetch_idx = cfg.get("fetch_index_price") - idx = float(fetch_idx(uly) or 0) if callable(fetch_idx) else 0.0 + from lib.hedge_plan.hedge_plan_live_positions_lib import resolve_option_index_px + 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 可能已是币;若为币且绝对值很小则 × 指数 - # resolve_option_leg_realized_pnl 对币本位通常返回币;统一展示用 preview 风格 if abs(opt_pnl) < 10 and idx > 0 and abs(opt_pnl * idx) > abs(opt_pnl): - # 更可能是币单位 opt_pnl_u = opt_pnl * idx else: opt_pnl_u = opt_pnl diff --git a/tests/test_hedge_po_mode.py b/tests/test_hedge_po_mode.py index b85d879..c25d6e8 100644 --- a/tests/test_hedge_po_mode.py +++ b/tests/test_hedge_po_mode.py @@ -172,6 +172,48 @@ class TestHedgePoMode(unittest.TestCase): self.assertEqual(p["exit_mode"], "profit") 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__": unittest.main()