修复 OKX 期权翻倍出场与币本位浮盈:中控/看板按指数折 U,避免 BTC 币数量当美金。
翻倍出场按全仓买一判定且对冲表异常不再整段跳过;面板按当前倍数展示需回收公式。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1977,6 +1977,21 @@
|
||||
return fmt(n, 2) + "倍";
|
||||
}
|
||||
|
||||
function profitExitHint(mult, req, premCcy) {
|
||||
const n = Number(mult);
|
||||
const m = Number.isFinite(n) && n > 0 ? n : 1;
|
||||
const needFactor = 1 + m;
|
||||
const factorTxt = Math.abs(needFactor - Math.round(needFactor)) < 1e-9
|
||||
? String(Math.round(needFactor))
|
||||
: fmt(needFactor, 2);
|
||||
let hint = formatProfitExitMultLabel(m) + "=盈利达权利金×" + fmt(m, 2)
|
||||
+ " → 需回收≥权利金×" + factorTxt;
|
||||
if (req != null) {
|
||||
hint += " · 即≥" + fmtPremiumAmt(req, premCcy) + " " + premCcy;
|
||||
}
|
||||
return hint;
|
||||
}
|
||||
|
||||
function renderProfitExitRow(p) {
|
||||
const inst = p.inst_id || "";
|
||||
if (p.hedge_plan_target) {
|
||||
@@ -1997,6 +2012,7 @@
|
||||
const statePe = String(p.profit_exit_state || (enabled ? "active" : "idle"));
|
||||
const req = p.profit_exit_required_recycle;
|
||||
const premCcy = posPremiumCcy(p);
|
||||
const hintMult = Number.isFinite(multNum) && multNum > 0 ? multNum : serverMult;
|
||||
let statusTxt = enabled ? ("监控中 · " + multLabel) : "未开启";
|
||||
if (enabled && statePe === "closing") statusTxt = "平仓挂单中 · " + multLabel;
|
||||
return (
|
||||
@@ -2013,7 +2029,7 @@
|
||||
'<span class="opt-target-armed">' + statusTxt + "</span>" +
|
||||
'<span class="muted opt-target-row-hint">' +
|
||||
(enabled
|
||||
? ("1倍=盈利=权利金" + (req != null ? (" · 需回收≥" + fmtPremiumAmt(req, premCcy) + " " + premCcy) : ""))
|
||||
? profitExitHint(hintMult, req, premCcy)
|
||||
: "开启后自选倍数;达标按买一限价平;可随时关闭") +
|
||||
"</span>" +
|
||||
"</div>"
|
||||
|
||||
@@ -74,71 +74,30 @@ def options_balances_usdt_equiv(options_snap: dict[str, Any] | None) -> dict[str
|
||||
|
||||
|
||||
def options_float_pnl_usdt(options_snap: dict[str, Any] | None) -> Optional[float]:
|
||||
"""期权浮盈合计(USDT).币本位按指数换算,勿把 ETH/BTC 数量当 U."""
|
||||
"""期权浮盈合计(USDT).币本位按指数换算,勿把 ETH/BTC 数量当 U.
|
||||
|
||||
优先逐仓 display_pnl_usdt_from_option_row(已含 BTC/ETH × 指数);
|
||||
回退 upl_total_usdc 时按「已是 USDT」处理——实例 hub 快照合计已折 U,
|
||||
不可再 × 指数,否则币本位会双计.
|
||||
"""
|
||||
snap = options_snap if isinstance(options_snap, dict) else {}
|
||||
if snap.get("enabled") is False or snap.get("ok") is False:
|
||||
return None
|
||||
|
||||
def _safe(v: Any) -> float | None:
|
||||
if v is None or v == "":
|
||||
return None
|
||||
try:
|
||||
return float(v)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
def _index_px() -> float | None:
|
||||
px = _safe(snap.get("options_index_px") or snap.get("index_px"))
|
||||
if px is not None and px > 0:
|
||||
return px
|
||||
for p in snap.get("positions") or []:
|
||||
if not isinstance(p, dict):
|
||||
continue
|
||||
px = _safe(p.get("idx_px") or p.get("idxPx"))
|
||||
if px is not None and px > 0:
|
||||
return px
|
||||
return None
|
||||
|
||||
def _row_is_coin(p: dict[str, Any]) -> bool:
|
||||
ccy = str(p.get("premium_ccy") or "").strip().upper()
|
||||
if ccy in ("ETH", "BTC"):
|
||||
return True
|
||||
if str(p.get("margin_mode") or "").strip().lower() == "coin":
|
||||
return True
|
||||
mid = str(p.get("inst_id") or "")
|
||||
return "-USD-" in mid.upper() and "_UM" not in mid.upper()
|
||||
|
||||
mode = str(snap.get("options_margin_mode") or snap.get("margin_mode") or "").strip().lower()
|
||||
snap_coin = mode == "coin"
|
||||
idx = _index_px()
|
||||
|
||||
try:
|
||||
from lib.options.options_positions_lib import display_pnl_from_option_row
|
||||
from lib.options.options_positions_lib import display_pnl_usdt_from_option_row
|
||||
|
||||
total_u = 0.0
|
||||
found = False
|
||||
missing_fx = False
|
||||
for p in snap.get("positions") or []:
|
||||
if not isinstance(p, dict):
|
||||
continue
|
||||
pnl = display_pnl_from_option_row(p)
|
||||
pnl = display_pnl_usdt_from_option_row(p)
|
||||
if pnl is None:
|
||||
continue
|
||||
found = True
|
||||
if snap_coin or _row_is_coin(p):
|
||||
px = _safe(p.get("idx_px") or p.get("idxPx"))
|
||||
if px is None or px <= 0:
|
||||
px = idx
|
||||
if px is None or px <= 0:
|
||||
missing_fx = True
|
||||
continue
|
||||
total_u += float(pnl) * float(px)
|
||||
else:
|
||||
total_u += float(pnl)
|
||||
if found and not missing_fx:
|
||||
return round(total_u, 4)
|
||||
if found and missing_fx and abs(total_u) > 1e-12:
|
||||
# 部分腿已换算成功时仍返回可得合计
|
||||
if found:
|
||||
return round(total_u, 4)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -146,16 +105,9 @@ def options_float_pnl_usdt(options_snap: dict[str, Any] | None) -> Optional[floa
|
||||
upl = snap.get("upl_total_usdc")
|
||||
if upl is not None:
|
||||
try:
|
||||
raw = float(upl)
|
||||
return round(float(upl), 4)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
if snap_coin or any(
|
||||
isinstance(p, dict) and _row_is_coin(p) for p in (snap.get("positions") or [])
|
||||
):
|
||||
if idx is None or idx <= 0:
|
||||
return None
|
||||
return round(raw * float(idx), 4)
|
||||
return round(raw, 4)
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -162,12 +162,12 @@ def _format_options_item(p: dict[str, Any], *, conn=None) -> dict[str, Any]:
|
||||
inst = str(p.get("inst_id") or p.get("instId") or "-").strip() or "-"
|
||||
opt_type = str(p.get("opt_type") or p.get("optType") or "").upper()
|
||||
label = "Call" if opt_type == "C" else "Put" if opt_type == "P" else (opt_type or "OPT")
|
||||
# 看板期权列:优先买一净盈亏,残档回退交易所 upl
|
||||
# 看板期权列:统一折成 U(币本位 BTC/ETH 权利金盈亏 × 指数)
|
||||
pnl = None
|
||||
try:
|
||||
from lib.options.options_positions_lib import display_pnl_from_option_row
|
||||
from lib.options.options_positions_lib import display_pnl_usdt_from_option_row
|
||||
|
||||
pnl = display_pnl_from_option_row(p)
|
||||
pnl = display_pnl_usdt_from_option_row(p)
|
||||
except Exception:
|
||||
pnl = None
|
||||
pos = _safe_float(p.get("pos"))
|
||||
|
||||
@@ -85,13 +85,13 @@ def build_options_hub_snapshot(cfg: dict[str, Any]) -> dict[str, Any]:
|
||||
conn.close()
|
||||
except Exception:
|
||||
target_monitors = []
|
||||
from lib.options.options_positions_lib import display_pnl_from_option_row
|
||||
from lib.options.options_positions_lib import display_pnl_usdt_from_option_row
|
||||
|
||||
upl_total = 0.0
|
||||
has_upl = False
|
||||
for p in positions:
|
||||
# 与持仓卡展示一致:优先买一净盈亏,残档回退交易所 upl
|
||||
pnl = display_pnl_from_option_row(p)
|
||||
# 合计一律折 U:币本位 × 指数,避免 BTC/ETH 币数量与 USDC 混加
|
||||
pnl = display_pnl_usdt_from_option_row(p)
|
||||
if pnl is None:
|
||||
continue
|
||||
has_upl = True
|
||||
|
||||
@@ -124,14 +124,42 @@ def display_pnl_from_option_row(row: dict[str, Any]) -> float | None:
|
||||
return _safe_float(row.get("upl"))
|
||||
|
||||
|
||||
def display_pnl_usdt_from_option_row(row: dict[str, Any]) -> float | None:
|
||||
"""顶栏/看板/中控「实时盈亏」用:币本位净盈亏按指数折 USDT;USDC 仓原样.
|
||||
|
||||
BTC/ETH 币本位若不折算,会被界面标成 U 而数值仍是币数量(BTC 尤为离谱).
|
||||
"""
|
||||
pnl = display_pnl_from_option_row(row)
|
||||
if pnl is None:
|
||||
return None
|
||||
ccy = str(row.get("premium_ccy") or "USDC").strip().upper() or "USDC"
|
||||
if not ccy or ccy == "USDC":
|
||||
# 兜底:按合约推断币本位,避免 premium_ccy 缺失时把 BTC 盈亏当 U
|
||||
inst = str(row.get("inst_id") or row.get("instId") or "")
|
||||
mode = str(row.get("margin_mode") or "").strip().lower()
|
||||
if mode == "coin" or (
|
||||
"-USD-" in inst.upper() and "_UM" not in inst.upper()
|
||||
):
|
||||
underly = str(row.get("underlying") or (inst.split("-")[0] if inst else "") or "").upper()
|
||||
if underly in ("ETH", "BTC"):
|
||||
ccy = underly
|
||||
if ccy in ("ETH", "BTC"):
|
||||
idx = _safe_float(row.get("idx_px") or row.get("idxPx") or row.get("index_px"))
|
||||
if idx is None or idx <= 0:
|
||||
# 无指数时无法折算,跳过该仓以免把「币数量」当成 U
|
||||
return None
|
||||
return float(pnl) * float(idx)
|
||||
return float(pnl)
|
||||
|
||||
|
||||
def sum_options_net_pnl_usdc(
|
||||
cfg: dict[str, Any],
|
||||
ex: Any,
|
||||
raw_positions: list[dict[str, Any]] | None = None,
|
||||
) -> float | None:
|
||||
"""
|
||||
期权浮盈合计(USDC),与顶栏实时盈亏/中控口径对齐为「净盈亏」:
|
||||
各仓买一可回收 − 权利金之和;残档则回退该仓交易所 upl.
|
||||
期权浮盈合计(USDT/USDC 口径),与顶栏实时盈亏/中控对齐:
|
||||
各仓买一可回收 − 权利金;币本位再 × 指数折成 U;残档回退该仓 upl(同样折算).
|
||||
获取失败返回 None;无持仓返回 0.
|
||||
"""
|
||||
raw = raw_positions
|
||||
@@ -145,12 +173,12 @@ def sum_options_net_pnl_usdc(
|
||||
total = 0.0
|
||||
found = False
|
||||
for p in positions:
|
||||
pnl = display_pnl_from_option_row(p)
|
||||
pnl = display_pnl_usdt_from_option_row(p)
|
||||
if pnl is None:
|
||||
continue
|
||||
found = True
|
||||
total += float(pnl)
|
||||
return round(total, 8) if found else (0.0 if not positions else None)
|
||||
return round(total, 4) if found else (0.0 if not positions else None)
|
||||
|
||||
|
||||
def build_display_option_positions(
|
||||
|
||||
@@ -211,13 +211,34 @@ def _estimate_recycle(
|
||||
pos: dict[str, Any],
|
||||
premium_paid: float | None,
|
||||
) -> float | None:
|
||||
"""翻倍出场用可回收估算.
|
||||
|
||||
用买一价 × 全部持仓张数(不吃深度上限):深度不够时仍应触发分批平仓;
|
||||
残档/无效买一则返回 None(禁止按假买盘自动平).
|
||||
"""
|
||||
from lib.options.options_positions_lib import attach_close_preview
|
||||
from lib.options.options_pricing_lib import total_premium
|
||||
|
||||
row = dict(pos)
|
||||
attach_close_preview(cfg, ex, row, premium_paid=premium_paid)
|
||||
preview = row.get("close_preview") if isinstance(row.get("close_preview"), dict) else {}
|
||||
if preview.get("bid_invalid"):
|
||||
return None
|
||||
levels = preview.get("levels") or []
|
||||
ct_mult = float(row.get("ct_mult") or 0.01)
|
||||
sheets = int(
|
||||
abs(
|
||||
_safe_float(row.get("avail_pos") if row.get("avail_pos") is not None else row.get("pos"))
|
||||
or 0
|
||||
)
|
||||
)
|
||||
if levels and sheets > 0 and ct_mult > 0:
|
||||
try:
|
||||
px = float(levels[0].get("px") or 0)
|
||||
except (TypeError, ValueError, AttributeError):
|
||||
px = 0.0
|
||||
if px > 0:
|
||||
return round(total_premium(px, sheets * ct_mult), 8)
|
||||
return _safe_float(preview.get("total_received"))
|
||||
|
||||
|
||||
@@ -303,7 +324,8 @@ def run_options_profit_exits(
|
||||
init_hedge_plan_tables(conn)
|
||||
hedge_managed = active_hedge_option_inst_ids(conn)
|
||||
except Exception:
|
||||
return 0
|
||||
# 对冲表异常时不得整段跳过翻倍出场
|
||||
hedge_managed = set()
|
||||
|
||||
rules = profit_exit_by_inst(conn)
|
||||
triggered = 0
|
||||
|
||||
@@ -79,12 +79,52 @@ class HubOptionsFundsLibTests(TestCase):
|
||||
"premium_ccy": "ETH",
|
||||
"upl": 0.0016,
|
||||
"idx_px": 2280.0,
|
||||
"close_preview": {"bid_invalid": True},
|
||||
}
|
||||
],
|
||||
}
|
||||
out = options_float_pnl_usdt(snap)
|
||||
self.assertAlmostEqual(out, round(0.0016 * 2280.0, 4), places=4)
|
||||
|
||||
def test_options_float_pnl_usdt_btc_coin_times_btc_index(self):
|
||||
from lib.hub.hub_options_funds_lib import options_float_pnl_usdt
|
||||
|
||||
snap = {
|
||||
"ok": True,
|
||||
"enabled": True,
|
||||
"options_margin_mode": "coin",
|
||||
# 默认指数若误用 ETH 也不应覆盖逐仓 BTC idx
|
||||
"options_index_px": 3000.0,
|
||||
"upl_total_usdc": -100.0,
|
||||
"positions": [
|
||||
{
|
||||
"inst_id": "BTC-USD-260327-100000-P",
|
||||
"margin_mode": "coin",
|
||||
"premium_ccy": "BTC",
|
||||
"upl": -0.001,
|
||||
"idx_px": 100000.0,
|
||||
"close_preview": {"bid_invalid": True},
|
||||
}
|
||||
],
|
||||
}
|
||||
out = options_float_pnl_usdt(snap)
|
||||
self.assertAlmostEqual(out, -100.0, places=4)
|
||||
|
||||
def test_options_float_pnl_usdt_fallback_already_usdt_no_double_fx(self):
|
||||
"""实例 hub 合计已折 U 时,回退不可再 × 指数."""
|
||||
from lib.hub.hub_options_funds_lib import options_float_pnl_usdt
|
||||
|
||||
snap = {
|
||||
"ok": True,
|
||||
"enabled": True,
|
||||
"options_margin_mode": "coin",
|
||||
"options_index_px": 100000.0,
|
||||
"upl_total_usdc": -100.0,
|
||||
"positions": [],
|
||||
}
|
||||
out = options_float_pnl_usdt(snap)
|
||||
self.assertAlmostEqual(out, -100.0, places=4)
|
||||
|
||||
def test_repair_double_counted_fund_entry(self):
|
||||
raw = {
|
||||
"funding_usdt": 586.82,
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
"""币本位期权浮盈折 U(BTC/ETH 不得把币数量直接当 U)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from lib.options.options_positions_lib import display_pnl_usdt_from_option_row
|
||||
|
||||
|
||||
class DisplayPnlUsdtTests(unittest.TestCase):
|
||||
def test_btc_coin_pnl_times_index(self):
|
||||
row = {
|
||||
"inst_id": "BTC-USD-260327-100000-P",
|
||||
"premium_ccy": "BTC",
|
||||
"margin_mode": "coin",
|
||||
"idx_px": 100000.0,
|
||||
"upl": -0.001,
|
||||
"close_preview": {"bid_invalid": True},
|
||||
}
|
||||
self.assertAlmostEqual(display_pnl_usdt_from_option_row(row), -100.0, places=4)
|
||||
|
||||
def test_btc_infers_ccy_when_premium_ccy_missing(self):
|
||||
row = {
|
||||
"inst_id": "BTC-USD-260327-100000-C",
|
||||
"margin_mode": "coin",
|
||||
"underlying": "BTC",
|
||||
"idx_px": 95000.0,
|
||||
"upl": 0.002,
|
||||
"close_preview": {"bid_invalid": True},
|
||||
}
|
||||
self.assertAlmostEqual(display_pnl_usdt_from_option_row(row), 190.0, places=4)
|
||||
|
||||
def test_usdc_unchanged(self):
|
||||
row = {
|
||||
"inst_id": "BTC-USD_UM-260327-100000-C",
|
||||
"premium_ccy": "USDC",
|
||||
"margin_mode": "usdc",
|
||||
"upl": -12.5,
|
||||
"close_preview": {"bid_invalid": True},
|
||||
}
|
||||
self.assertAlmostEqual(display_pnl_usdt_from_option_row(row), -12.5, places=4)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,39 @@
|
||||
"""翻倍出场:买一价×全仓张数判定达标(不受买一深度截断)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from lib.options.options_profit_exit_lib import _estimate_recycle, profit_exit_hit
|
||||
|
||||
|
||||
class ProfitExitHitTests(unittest.TestCase):
|
||||
def test_hit_at_0_3_mult(self):
|
||||
self.assertTrue(profit_exit_hit(premium_paid=1.0, recycle_usdc=1.3, mult=0.3))
|
||||
self.assertFalse(profit_exit_hit(premium_paid=1.0, recycle_usdc=1.29, mult=0.3))
|
||||
|
||||
def test_estimate_uses_full_sheets_not_depth(self):
|
||||
pos = {"inst_id": "ETH-USD-260927-2700-P", "pos": 100, "ct_mult": 0.01}
|
||||
preview = {
|
||||
"bid_invalid": False,
|
||||
"levels": [{"px": 0.2, "sheets": 10}],
|
||||
"total_received": 0.02,
|
||||
}
|
||||
|
||||
def fake_attach(cfg, ex, row, premium_paid=None):
|
||||
row["close_preview"] = preview
|
||||
row["ct_mult"] = 0.01
|
||||
row["pos"] = 100
|
||||
return row
|
||||
|
||||
with patch(
|
||||
"lib.options.options_positions_lib.attach_close_preview",
|
||||
side_effect=fake_attach,
|
||||
):
|
||||
recv = _estimate_recycle({}, object(), pos, 1.0)
|
||||
self.assertAlmostEqual(float(recv), 0.2, places=6)
|
||||
self.assertTrue(profit_exit_hit(premium_paid=0.15, recycle_usdc=float(recv), mult=0.3))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user