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:
@@ -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
|
||||||
|
```
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user