修复币本位期权残档判定:内在价值按币报价(S-K)/S,避免与美元点差混比误杀有效买一
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -13,6 +13,7 @@ import ccxt
|
|||||||
from lib.options.options_pricing_lib import (
|
from lib.options.options_pricing_lib import (
|
||||||
expiry_breakeven_from_ask,
|
expiry_breakeven_from_ask,
|
||||||
idx_distance_to_be,
|
idx_distance_to_be,
|
||||||
|
intrinsic_px_per_unit,
|
||||||
is_shallow_itm,
|
is_shallow_itm,
|
||||||
option_moneyness,
|
option_moneyness,
|
||||||
option_moneyness_label,
|
option_moneyness_label,
|
||||||
@@ -251,15 +252,6 @@ def tick_sz_and_ct_mult(
|
|||||||
return tick_sz, ct_mult or 0.01
|
return tick_sz, ct_mult or 0.01
|
||||||
|
|
||||||
|
|
||||||
def _intrinsic_px_per_unit(opt_type: str, strike: float, index_px: float) -> float | None:
|
|
||||||
o = (opt_type or "").upper()
|
|
||||||
if o == "C" and index_px > strike:
|
|
||||||
return float(index_px) - float(strike)
|
|
||||||
if o == "P" and index_px < strike:
|
|
||||||
return float(strike) - float(index_px)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _resolve_chain_quote(
|
def _resolve_chain_quote(
|
||||||
*,
|
*,
|
||||||
ticker: dict[str, Any],
|
ticker: dict[str, Any],
|
||||||
@@ -267,6 +259,7 @@ def _resolve_chain_quote(
|
|||||||
opt_type: str,
|
opt_type: str,
|
||||||
strike: float,
|
strike: float,
|
||||||
index_px: float,
|
index_px: float,
|
||||||
|
inst_id: str | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""链列表报价:卖一缺失时用标记价/内在价值估算(深度实值常见无卖一)."""
|
"""链列表报价:卖一缺失时用标记价/内在价值估算(深度实值常见无卖一)."""
|
||||||
tick_sz = meta.get("tickSz")
|
tick_sz = meta.get("tickSz")
|
||||||
@@ -276,12 +269,13 @@ def _resolve_chain_quote(
|
|||||||
ask_sz = _safe_float(ticker.get("askSz"))
|
ask_sz = _safe_float(ticker.get("askSz"))
|
||||||
bid_sz = _safe_float(ticker.get("bidSz"))
|
bid_sz = _safe_float(ticker.get("bidSz"))
|
||||||
ask_estimated = False
|
ask_estimated = False
|
||||||
|
iid = (inst_id or str(meta.get("instId") or "")).strip()
|
||||||
|
|
||||||
if ask is None and mark is not None and mark > 0:
|
if ask is None and mark is not None and mark > 0:
|
||||||
ask = round_option_px(mark, tick_sz, "buy")
|
ask = round_option_px(mark, tick_sz, "buy")
|
||||||
ask_estimated = True
|
ask_estimated = True
|
||||||
if ask is None:
|
if ask is None:
|
||||||
intrinsic = _intrinsic_px_per_unit(opt_type, strike, index_px)
|
intrinsic = intrinsic_px_per_unit(opt_type, strike, index_px, inst_id=iid or None)
|
||||||
if intrinsic is not None and intrinsic > 0:
|
if intrinsic is not None and intrinsic > 0:
|
||||||
ask = round_option_px(intrinsic, tick_sz, "buy")
|
ask = round_option_px(intrinsic, tick_sz, "buy")
|
||||||
ask_estimated = True
|
ask_estimated = True
|
||||||
@@ -289,7 +283,7 @@ def _resolve_chain_quote(
|
|||||||
if bid is None and mark is not None and mark > 0:
|
if bid is None and mark is not None and mark > 0:
|
||||||
bid = round_option_px(mark, tick_sz, "sell")
|
bid = round_option_px(mark, tick_sz, "sell")
|
||||||
if bid is None:
|
if bid is None:
|
||||||
intrinsic = _intrinsic_px_per_unit(opt_type, strike, index_px)
|
intrinsic = intrinsic_px_per_unit(opt_type, strike, index_px, inst_id=iid or None)
|
||||||
if intrinsic is not None and intrinsic > 0:
|
if intrinsic is not None and intrinsic > 0:
|
||||||
bid = round_option_px(intrinsic, tick_sz, "sell")
|
bid = round_option_px(intrinsic, tick_sz, "sell")
|
||||||
|
|
||||||
@@ -878,6 +872,7 @@ def build_option_chain(
|
|||||||
opt_type=opt_type,
|
opt_type=opt_type,
|
||||||
strike=strike,
|
strike=strike,
|
||||||
index_px=idx,
|
index_px=idx,
|
||||||
|
inst_id=inst_id,
|
||||||
)
|
)
|
||||||
ask = q["ask"]
|
ask = q["ask"]
|
||||||
bid = q["bid"]
|
bid = q["bid"]
|
||||||
|
|||||||
@@ -58,7 +58,13 @@ def _pos_close_refs(ex: Any, pos: dict[str, Any], quote: dict[str, Any] | None =
|
|||||||
if strike is None:
|
if strike is None:
|
||||||
strike = ps
|
strike = ps
|
||||||
idx = _safe_float(pos.get("idxPx")) or _safe_float((quote or {}).get("index_px"))
|
idx = _safe_float(pos.get("idxPx")) or _safe_float((quote or {}).get("index_px"))
|
||||||
return close_ref_prices(mark_px=mark, opt_type=str(opt_type or ""), strike=strike, index_px=idx)
|
return close_ref_prices(
|
||||||
|
mark_px=mark,
|
||||||
|
opt_type=str(opt_type or ""),
|
||||||
|
strike=strike,
|
||||||
|
index_px=idx,
|
||||||
|
inst_id=inst_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _avail_sheets(pos: dict[str, Any]) -> int:
|
def _avail_sheets(pos: dict[str, Any]) -> int:
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ def attach_close_preview(
|
|||||||
row.get("opt_type") or row.get("optType"),
|
row.get("opt_type") or row.get("optType"),
|
||||||
_safe_float(row.get("strike") or row.get("stk")),
|
_safe_float(row.get("strike") or row.get("stk")),
|
||||||
_safe_float(row.get("idx_px") or row.get("idxPx")),
|
_safe_float(row.get("idx_px") or row.get("idxPx")),
|
||||||
|
inst_id=inst_id,
|
||||||
|
margin_mode=row.get("margin_mode"),
|
||||||
)
|
)
|
||||||
# 与实盘一致:只按买一估算本轮可平
|
# 与实盘一致:只按买一估算本轮可平
|
||||||
preview = estimate_close_by_bids(
|
preview = estimate_close_by_bids(
|
||||||
|
|||||||
@@ -64,7 +64,46 @@ def _safe_px(v: Any) -> float | None:
|
|||||||
return x if x > 0 else None
|
return x if x > 0 else None
|
||||||
|
|
||||||
|
|
||||||
def intrinsic_px_per_unit(opt_type: str | None, strike: float | None, index_px: float | None) -> float | None:
|
def _quote_in_coin_from_context(
|
||||||
|
*,
|
||||||
|
quote_in_coin: bool | None = None,
|
||||||
|
inst_id: str | None = None,
|
||||||
|
margin_mode: str | None = None,
|
||||||
|
) -> bool:
|
||||||
|
"""币本位(ETH-USD/BTC-USD)权利金按币报价;USDC(USD_UM)按美元点差."""
|
||||||
|
if quote_in_coin is not None:
|
||||||
|
return bool(quote_in_coin)
|
||||||
|
if inst_id:
|
||||||
|
try:
|
||||||
|
from lib.options.options_margin_mode_lib import MODE_COIN, margin_mode_from_inst_id
|
||||||
|
|
||||||
|
return margin_mode_from_inst_id(inst_id) == MODE_COIN
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if margin_mode is not None:
|
||||||
|
try:
|
||||||
|
from lib.options.options_margin_mode_lib import is_coin_margin_mode
|
||||||
|
|
||||||
|
return is_coin_margin_mode(margin_mode)
|
||||||
|
except Exception:
|
||||||
|
return str(margin_mode).strip().lower() in ("coin", "coin_margin", "crypto")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def intrinsic_px_per_unit(
|
||||||
|
opt_type: str | None,
|
||||||
|
strike: float | None,
|
||||||
|
index_px: float | None,
|
||||||
|
*,
|
||||||
|
quote_in_coin: bool | None = None,
|
||||||
|
inst_id: str | None = None,
|
||||||
|
margin_mode: str | None = None,
|
||||||
|
) -> float | None:
|
||||||
|
"""
|
||||||
|
与盘口同单位的内在价值(每 1 标的).
|
||||||
|
- USDC / USD_UM: 美元点差 max(0, S−K) / max(0, K−S)
|
||||||
|
- 币本位 ETH-USD / BTC-USD: 币报价 max(0, S−K)/S / max(0, K−S)/S
|
||||||
|
"""
|
||||||
o = (opt_type or "").strip().upper()
|
o = (opt_type or "").strip().upper()
|
||||||
if strike is None or index_px is None:
|
if strike is None or index_px is None:
|
||||||
return None
|
return None
|
||||||
@@ -74,10 +113,18 @@ def intrinsic_px_per_unit(opt_type: str | None, strike: float | None, index_px:
|
|||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return None
|
return None
|
||||||
if o == "C" and idx > k:
|
if o == "C" and idx > k:
|
||||||
return idx - k
|
points = idx - k
|
||||||
if o == "P" and idx < k:
|
elif o == "P" and idx < k:
|
||||||
return k - idx
|
points = k - idx
|
||||||
return None
|
else:
|
||||||
|
return None
|
||||||
|
if _quote_in_coin_from_context(
|
||||||
|
quote_in_coin=quote_in_coin, inst_id=inst_id, margin_mode=margin_mode
|
||||||
|
):
|
||||||
|
if idx <= 0:
|
||||||
|
return None
|
||||||
|
return points / idx
|
||||||
|
return points
|
||||||
|
|
||||||
|
|
||||||
def is_stub_bid_px(
|
def is_stub_bid_px(
|
||||||
@@ -128,9 +175,19 @@ def close_ref_prices(
|
|||||||
opt_type: str | None = None,
|
opt_type: str | None = None,
|
||||||
strike: float | None = None,
|
strike: float | None = None,
|
||||||
index_px: float | None = None,
|
index_px: float | None = None,
|
||||||
|
quote_in_coin: bool | None = None,
|
||||||
|
inst_id: str | None = None,
|
||||||
|
margin_mode: str | None = None,
|
||||||
) -> tuple[float | None, float | None]:
|
) -> tuple[float | None, float | None]:
|
||||||
"""返回 (mark_px, intrinsic_px) 供残档判断."""
|
"""返回 (mark_px, intrinsic_px) 供残档判断;intrinsic 与盘口同单位."""
|
||||||
return _safe_px(mark_px), intrinsic_px_per_unit(opt_type, strike, index_px)
|
return _safe_px(mark_px), intrinsic_px_per_unit(
|
||||||
|
opt_type,
|
||||||
|
strike,
|
||||||
|
index_px,
|
||||||
|
quote_in_coin=quote_in_coin,
|
||||||
|
inst_id=inst_id,
|
||||||
|
margin_mode=margin_mode,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def filter_bids_for_close(
|
def filter_bids_for_close(
|
||||||
|
|||||||
@@ -33,7 +33,13 @@ def _pos_close_refs(ex: Any, pos: dict[str, Any], quote: dict[str, Any] | None =
|
|||||||
if strike is None:
|
if strike is None:
|
||||||
strike = ps
|
strike = ps
|
||||||
idx = _safe_float(pos.get("idxPx")) or _safe_float((quote or {}).get("index_px"))
|
idx = _safe_float(pos.get("idxPx")) or _safe_float((quote or {}).get("index_px"))
|
||||||
return close_ref_prices(mark_px=mark, opt_type=str(opt_type or ""), strike=strike, index_px=idx)
|
return close_ref_prices(
|
||||||
|
mark_px=mark,
|
||||||
|
opt_type=str(opt_type or ""),
|
||||||
|
strike=strike,
|
||||||
|
index_px=idx,
|
||||||
|
inst_id=inst_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def ensure_target_tables(conn: sqlite3.Connection) -> None:
|
def ensure_target_tables(conn: sqlite3.Connection) -> None:
|
||||||
|
|||||||
@@ -267,6 +267,35 @@ def test_stub_bid_blocks_auto_close_estimate():
|
|||||||
assert good["covered_sheets"] == 10
|
assert good["covered_sheets"] == 10
|
||||||
|
|
||||||
|
|
||||||
|
def test_intrinsic_px_coin_vs_usdc_units():
|
||||||
|
from lib.options.options_pricing_lib import intrinsic_px_per_unit, is_stub_bid_px
|
||||||
|
|
||||||
|
# USDC / 默认:美元点差
|
||||||
|
assert intrinsic_px_per_unit("C", 2250, 2274) == 24.0
|
||||||
|
assert intrinsic_px_per_unit("C", 2250, 2274, margin_mode="usdc") == 24.0
|
||||||
|
|
||||||
|
# 币本位:与盘口同单位的币报价 (S−K)/S
|
||||||
|
coin_iv = intrinsic_px_per_unit("C", 2250, 2274, quote_in_coin=True)
|
||||||
|
assert coin_iv is not None
|
||||||
|
assert abs(coin_iv - 24.0 / 2274.0) < 1e-12
|
||||||
|
assert abs(
|
||||||
|
intrinsic_px_per_unit("C", 2250, 2274, inst_id="ETH-USD-260822-2250-C") - 24.0 / 2274.0
|
||||||
|
) < 1e-12
|
||||||
|
# USD_UM 仍为点差
|
||||||
|
assert intrinsic_px_per_unit("C", 2250, 2274, inst_id="ETH-USD_UM-260822-2250-C") == 24.0
|
||||||
|
|
||||||
|
# 复现线上误杀:把点差当内在价值会把正常买一判残档
|
||||||
|
wrong_stub, _ = is_stub_bid_px(0.023, mark_px=0.0241, intrinsic_px=23.58)
|
||||||
|
assert wrong_stub is True
|
||||||
|
# 币报价内在价值后,买一贴近标记价应有效
|
||||||
|
ok_stub, _ = is_stub_bid_px(0.023, mark_px=0.0241, intrinsic_px=coin_iv)
|
||||||
|
assert ok_stub is False
|
||||||
|
|
||||||
|
put_iv = intrinsic_px_per_unit("P", 2300, 2274, quote_in_coin=True)
|
||||||
|
assert put_iv is not None
|
||||||
|
assert abs(put_iv - 26.0 / 2274.0) < 1e-12
|
||||||
|
|
||||||
|
|
||||||
def test_expiry_breakeven_from_ask():
|
def test_expiry_breakeven_from_ask():
|
||||||
from lib.options.options_pricing_lib import expiry_breakeven_from_ask
|
from lib.options.options_pricing_lib import expiry_breakeven_from_ask
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user