From 57cca5554e98568ac6eb4d8117bec184c262e4db Mon Sep 17 00:00:00 2001 From: dekun Date: Sun, 23 Aug 2026 08:15:33 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=92=E8=BD=AC=E9=BB=98=E8=AE=A4=E6=8A=98?= =?UTF-8?q?=E5=8F=A0=EF=BC=8C=E4=BF=AE=E5=A4=8D=E5=B8=81=E6=9C=AC=E4=BD=8D?= =?UTF-8?q?=E5=88=B0=E6=9C=9F=E5=B9=B3=E8=A1=A1=E8=AE=A1=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 币本位权利金为币报价,到期平衡按 OKX 结算公式 K/(1±p) 计算;链/持仓/跨式平衡带同步修正。 Co-authored-by: Cursor --- lib/common/static/options_panel.js | 19 +++++--- lib/exchange/okx_options_lib.py | 6 +++ lib/options/options_pricing_lib.py | 56 +++++++++++++++++++++--- lib/options/templates/options_panel.html | 4 +- tests/test_options_pricing.py | 31 +++++++++++++ 5 files changed, 103 insertions(+), 13 deletions(-) diff --git a/lib/common/static/options_panel.js b/lib/common/static/options_panel.js index 96ad516..4b05140 100644 --- a/lib/common/static/options_panel.js +++ b/lib/common/static/options_panel.js @@ -878,12 +878,21 @@ return Math.round((c + p) * 10000) / 10000; } - function formatStraddleBand(strike, combinedAsk) { - const per = combinedAsk; - if (strike == null || per == null) return "—"; + function formatStraddleBand(strike, combinedAsk, callAsk, putAsk) { const k = Number(strike); + if (!Number.isFinite(k)) return "—"; + if (isCoinMarginMode()) { + const pc = Number(callAsk); + const pp = Number(putAsk); + if (!Number.isFinite(pc) || !Number.isFinite(pp) || pc <= 0 || pp <= 0 || pc >= 1) return "—"; + const lo = Math.round((k / (1 + pp)) * 10) / 10; + const hi = Math.round((k / (1 - pc)) * 10) / 10; + return lo.toFixed(0) + " ~ " + hi.toFixed(0); + } + const per = combinedAsk; + if (per == null) return "—"; const d = Number(per); - if (!Number.isFinite(k) || !Number.isFinite(d)) return "—"; + if (!Number.isFinite(d)) return "—"; const lo = Math.round((k - d) * 10) / 10; const hi = Math.round((k + d) * 10) / 10; return lo.toFixed(0) + " ~ " + hi.toFixed(0); @@ -1577,7 +1586,7 @@ '' + pickBtnHtml(call && call.inst_id) + "" + '' + row.strike + "" + '' + formatStraddlePremiumCell(call && call.ask, put && put.ask) + "" + - '' + formatStraddleBand(row.strike, combined) + "" + + '' + formatStraddleBand(row.strike, combined, call && call.ask, put && put.ask) + "" + '' + (put ? moneynessBadge(put) : "—") + "" + '' + (put ? fmtPxSz(put.ask, put.ask_sz, put.ask_estimated) : "—") + "" + '' + pickBtnHtml(put && put.inst_id) + ""; diff --git a/lib/exchange/okx_options_lib.py b/lib/exchange/okx_options_lib.py index 0055a06..04edc80 100644 --- a/lib/exchange/okx_options_lib.py +++ b/lib/exchange/okx_options_lib.py @@ -816,6 +816,7 @@ def build_option_chain( family = f"{u}-USD_UM" uly = f"{u}-USD" idx = index_px if index_px is not None else fetch_index_price(ex, uly) + chain_margin = "usdc" if "_UM" in family.upper() else "coin" now_ms = time.time() * 1000 max_ms = now_ms + max_dte_days * 86400 * 1000 instruments_err = "" @@ -884,6 +885,8 @@ def build_option_chain( strike=strike, ask_px=ask, mark_px=mark, + inst_id=inst_id, + margin_mode=chain_margin, ) mny = option_moneyness(opt_type=opt_type, strike=strike, index_px=idx) exp_key = str(exp_ms) @@ -1028,6 +1031,7 @@ def quote_option_contract(ex: ccxt.okx, inst_id: str) -> dict[str, Any]: strike=strike, ask_px=book_ask if can_open else None, mark_px=mark, + inst_id=inst_id, ) return { "ok": True, @@ -1815,6 +1819,8 @@ def format_position_row( strike=strike, avg_px=avg, be_px_api=_safe_float(pos.get("bePx")), + inst_id=inst_id, + margin_mode=row_mode, ) close_be = close_breakeven_idx( opt_type=str(opt_type or ""), diff --git a/lib/options/options_pricing_lib.py b/lib/options/options_pricing_lib.py index d591f16..f679af7 100644 --- a/lib/options/options_pricing_lib.py +++ b/lib/options/options_pricing_lib.py @@ -438,10 +438,20 @@ def expiry_breakeven_from_ask( strike: float | None, ask_px: float | None, mark_px: float | None = None, + quote_in_coin: bool | None = None, + inst_id: str | None = None, + margin_mode: str | None = None, ) -> float | None: """买入前预估到期平衡:权利金按卖一;无卖一时回退标记价.""" prem = ask_px if ask_px is not None and ask_px > 0 else mark_px - return expiry_breakeven_px(opt_type=opt_type, strike=strike, avg_px=prem) + return expiry_breakeven_px( + opt_type=opt_type, + strike=strike, + avg_px=prem, + quote_in_coin=quote_in_coin, + inst_id=inst_id, + margin_mode=margin_mode, + ) def expiry_breakeven_px( @@ -450,17 +460,39 @@ def expiry_breakeven_px( strike: float | None, avg_px: float | None, be_px_api: float | None = None, + quote_in_coin: bool | None = None, + inst_id: str | None = None, + margin_mode: str | None = None, ) -> float | None: """到期平衡点:持有至到期时标的指数盈亏为 0 的价格.优先 OKX bePx.""" if be_px_api is not None and be_px_api > 0: return round(float(be_px_api), 2) if strike is None or avg_px is None: return None + try: + k = float(strike) + p = float(avg_px) + except (TypeError, ValueError): + return None + if p <= 0: + return None o = (opt_type or "").upper() + coin = _quote_in_coin_from_context( + quote_in_coin=quote_in_coin, inst_id=inst_id, margin_mode=margin_mode + ) + if coin: + # 币本位:权利金为币报价;到期结算 payoff 亦为币 → K/(1±p) + if o == "C": + if p >= 1: + return None + return round(k / (1 - p), 2) + if o == "P": + return round(k / (1 + p), 2) + return None if o == "C": - return round(strike + avg_px, 2) + return round(k + p, 2) if o == "P": - return round(strike - avg_px, 2) + return round(k - p, 2) return None @@ -617,12 +649,24 @@ def straddle_premium_total( def straddle_breakeven_band( strike: float | None, - combined_ask_per_unit: float | None, + combined_ask_per_unit: float | None = None, + *, + call_ask: float | None = None, + put_ask: float | None = None, + quote_in_coin: bool = False, ) -> tuple[float | None, float | None]: - """跨式到期平衡带:下平衡 ~ 上平衡(按双卖一报价和).""" - if strike is None or combined_ask_per_unit is None: + """跨式到期平衡带:下平衡 ~ 上平衡.""" + if strike is None: return None, None k = float(strike) + if quote_in_coin: + pc = _safe_px(call_ask) + pp = _safe_px(put_ask) + if pc is None or pp is None or pc <= 0 or pp <= 0 or pc >= 1: + return None, None + return round(k / (1 + pp), 2), round(k / (1 - pc), 2) + if combined_ask_per_unit is None: + return None, None d = float(combined_ask_per_unit) return round(k - d, 2), round(k + d, 2) diff --git a/lib/options/templates/options_panel.html b/lib/options/templates/options_panel.html index 2e9596a..99fcc3c 100644 --- a/lib/options/templates/options_panel.html +++ b/lib/options/templates/options_panel.html @@ -345,7 +345,7 @@ -
+
划转 收起 @@ -380,4 +380,4 @@ - + diff --git a/tests/test_options_pricing.py b/tests/test_options_pricing.py index 4b85102..b0d6cd4 100644 --- a/tests/test_options_pricing.py +++ b/tests/test_options_pricing.py @@ -331,6 +331,37 @@ def test_expiry_breakeven_call_put(): assert expiry_breakeven_px(opt_type="P", strike=3500, avg_px=15.6) == 3484.4 +def test_expiry_breakeven_coin_margin(): + from lib.options.options_pricing_lib import expiry_breakeven_from_ask, expiry_breakeven_px + + # ETH-USD 币本位:卖一 0.0165 → 到期平衡 K/(1-p),非 K+p + assert expiry_breakeven_px( + opt_type="C", strike=2390, avg_px=0.0165, margin_mode="coin" + ) == round(2390 / (1 - 0.0165), 2) + assert expiry_breakeven_px( + opt_type="P", strike=2450, avg_px=0.0161, margin_mode="coin" + ) == round(2450 / (1 + 0.0161), 2) + assert expiry_breakeven_from_ask( + opt_type="C", + strike=2425, + ask_px=0.0187, + inst_id="ETH-USD-260823-2425-C", + ) == round(2425 / (1 - 0.0187), 2) + + +def test_straddle_breakeven_band_coin(): + from lib.options.options_pricing_lib import straddle_breakeven_band + + lo, hi = straddle_breakeven_band( + 2425, + quote_in_coin=True, + call_ask=0.0187, + put_ask=0.0253, + ) + assert lo == round(2425 / (1 + 0.0253), 2) + assert hi == round(2425 / (1 - 0.0187), 2) + + def test_close_breakeven_at_mark_equals_avg(): from lib.options.options_pricing_lib import close_breakeven_idx