Floor option close at intrinsic to fix expiry garbage quotes.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -12,7 +12,12 @@ from ..models.db import Database, get_db
|
||||
from ..strategy.session import get_session
|
||||
from .ledger import Ledger
|
||||
from .liquidity import bid_covers_eth, bid_mark_ok, contracts_for_eth
|
||||
from .pricing import option_fill, perp_fill
|
||||
from .pricing import (
|
||||
option_fill,
|
||||
option_intrinsic,
|
||||
perp_fill,
|
||||
resolve_option_close_bid,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
@@ -71,6 +76,61 @@ class Matcher:
|
||||
)
|
||||
return CloseResult(ok=False, detail=detail, liquidity_wait=True)
|
||||
|
||||
def _group_strike(self, group_id: str, option_inst_id: str) -> float | None:
|
||||
g = self.db.fetchone(
|
||||
"SELECT strike FROM groups WHERE group_id=?", (group_id,)
|
||||
)
|
||||
if g is not None and g["strike"] is not None:
|
||||
try:
|
||||
return float(g["strike"])
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
try:
|
||||
from ..exchange.okx.parse import parse_option_inst_id
|
||||
|
||||
_, stk, _ = parse_option_inst_id(option_inst_id)
|
||||
if stk is not None:
|
||||
return float(stk)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
from ..exchange.binance.parse import parse_option_symbol
|
||||
|
||||
_, stk, _ = parse_option_symbol(option_inst_id)
|
||||
if stk is not None:
|
||||
return float(stk)
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def _close_spot_px(self, snap: Any) -> float | None:
|
||||
if getattr(snap, "index_px", None) is not None:
|
||||
try:
|
||||
px = float(snap.index_px)
|
||||
if px > 0:
|
||||
return px
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
perp = getattr(snap, "perp", None)
|
||||
if not perp:
|
||||
return None
|
||||
for attr in ("mark_px", "last"):
|
||||
v = getattr(perp, attr, None)
|
||||
if v is not None:
|
||||
try:
|
||||
px = float(v)
|
||||
if px > 0:
|
||||
return px
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
if perp.bid is not None and perp.ask is not None:
|
||||
return (float(perp.bid) + float(perp.ask)) / 2.0
|
||||
if perp.bid is not None:
|
||||
return float(perp.bid)
|
||||
if perp.ask is not None:
|
||||
return float(perp.ask)
|
||||
return None
|
||||
|
||||
def open_group(
|
||||
self,
|
||||
*,
|
||||
@@ -269,7 +329,8 @@ class Matcher:
|
||||
def close_group(self, *, reason: str, bypass_liquidity: bool = False) -> CloseResult:
|
||||
"""
|
||||
全平一组。默认校验期权买一深度 + 买一/标记偏差(默认≤30%)。
|
||||
bypass_liquidity=True:紧急全平可绕过(仍需有可用买一价才能成交;无买一时用标记近似)。
|
||||
bypass_liquidity=True:紧急/到期可绕过深度闸门;平仓价取 max(买一, 标记, 内在价值),
|
||||
避免到期垃圾盘口把实值期权按近零价卖掉。
|
||||
成交顺序:先平期权(薄)→ 再瞬时平永续(对冲先留着);永续盘口失败则回滚期权入账。
|
||||
"""
|
||||
s = get_settings()
|
||||
@@ -302,6 +363,14 @@ class Matcher:
|
||||
)
|
||||
|
||||
close_bid = oq.bid
|
||||
strike = self._group_strike(group_id, option_inst_id)
|
||||
spot = self._close_spot_px(snap)
|
||||
intrinsic: float | None = None
|
||||
if strike is not None and spot is not None:
|
||||
intrinsic = option_intrinsic(
|
||||
option_side=option_side, strike=strike, spot=spot
|
||||
)
|
||||
|
||||
if not bypass_liquidity:
|
||||
if close_bid is None:
|
||||
return self._liquidity_wait(group_id, "期权买一不可用")
|
||||
@@ -316,12 +385,29 @@ class Matcher:
|
||||
)
|
||||
if not ok_dev:
|
||||
return self._liquidity_wait(group_id, why)
|
||||
resolved = resolve_option_close_bid(
|
||||
bid=float(close_bid),
|
||||
mark=oq.mark_px,
|
||||
intrinsic=intrinsic,
|
||||
bypass_liquidity=False,
|
||||
)
|
||||
if resolved is None:
|
||||
return self._liquidity_wait(group_id, "期权平仓价不可用")
|
||||
close_bid = resolved
|
||||
else:
|
||||
# 紧急:优先买一,否则用标记价近似成交(SIM)
|
||||
if close_bid is None:
|
||||
close_bid = oq.mark_px
|
||||
if close_bid is None:
|
||||
return CloseResult(ok=False, detail="紧急全平失败:无买一/标记价")
|
||||
# 到期/紧急:买一/标记可能枯死,用 max(买一, 标记, 内在价值)
|
||||
resolved = resolve_option_close_bid(
|
||||
bid=close_bid,
|
||||
mark=oq.mark_px,
|
||||
intrinsic=intrinsic,
|
||||
bypass_liquidity=True,
|
||||
)
|
||||
if resolved is None:
|
||||
return CloseResult(
|
||||
ok=False,
|
||||
detail="紧急全平失败:无买一/标记/内在价值",
|
||||
)
|
||||
close_bid = resolved
|
||||
|
||||
fee_rate = self._fee_rate()
|
||||
perp_side = str(pos["perp_side"])
|
||||
@@ -455,6 +541,9 @@ class Matcher:
|
||||
"net": net,
|
||||
"close_sequence": ["option", "perp"],
|
||||
"cash_delta": opt_cash + perp_pnl - pf.fee,
|
||||
"option_close_bid": float(close_bid),
|
||||
"option_intrinsic": intrinsic,
|
||||
"settle_spot": spot,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -17,6 +17,49 @@ class PriceResult:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
def option_intrinsic(*, option_side: str, strike: float, spot: float) -> float:
|
||||
"""多头期权内在价值(USDT/ETH)。call=max(S−K,0),put=max(K−S,0)。"""
|
||||
s = float(spot)
|
||||
k = float(strike)
|
||||
side = str(option_side).lower().strip()
|
||||
if side in ("call", "c"):
|
||||
return max(s - k, 0.0)
|
||||
if side in ("put", "p"):
|
||||
return max(k - s, 0.0)
|
||||
return 0.0
|
||||
|
||||
|
||||
def resolve_option_close_bid(
|
||||
*,
|
||||
bid: float | None,
|
||||
mark: float | None,
|
||||
intrinsic: float | None,
|
||||
bypass_liquidity: bool,
|
||||
) -> float | None:
|
||||
"""
|
||||
平仓用买一价;多头卖出不得低于内在价值(SIM 防到期垃圾盘口)。
|
||||
bypass 时:买一缺失可用标记/内在价值兜底。
|
||||
"""
|
||||
candidates: list[float] = []
|
||||
if bid is not None and bid >= 0:
|
||||
candidates.append(float(bid))
|
||||
if bypass_liquidity and mark is not None and mark >= 0:
|
||||
candidates.append(float(mark))
|
||||
if intrinsic is not None and intrinsic >= 0:
|
||||
candidates.append(float(intrinsic))
|
||||
if not candidates:
|
||||
return None
|
||||
# 常规:有买一时,仍用 max(买一, 内在价值) 抬到合理底价
|
||||
# bypass:max(买一, 标记, 内在价值)
|
||||
if bypass_liquidity:
|
||||
return max(candidates)
|
||||
if bid is None:
|
||||
return None
|
||||
if intrinsic is not None and intrinsic >= 0:
|
||||
return max(float(bid), float(intrinsic))
|
||||
return float(bid)
|
||||
|
||||
|
||||
def perp_fill(
|
||||
*,
|
||||
side: str,
|
||||
|
||||
@@ -142,3 +142,40 @@ def test_expiry_close() -> None:
|
||||
assert d2.reason == "expiry"
|
||||
d3 = check_expiry_close(expiry_ms=1_000, now_ms=1_001)
|
||||
assert d3.should_close is True
|
||||
|
||||
|
||||
def test_option_intrinsic_and_close_bid_floor() -> None:
|
||||
from app.sim.pricing import option_intrinsic, resolve_option_close_bid
|
||||
|
||||
assert option_intrinsic(option_side="call", strike=1860, spot=1882) == 22.0
|
||||
assert option_intrinsic(option_side="put", strike=1860, spot=1882) == 0.0
|
||||
assert option_intrinsic(option_side="put", strike=1860, spot=1840) == 20.0
|
||||
|
||||
# 到期垃圾买一 0.2,内在价值 22 → 抬到 22
|
||||
assert (
|
||||
resolve_option_close_bid(
|
||||
bid=0.2, mark=0.2, intrinsic=22.0, bypass_liquidity=True
|
||||
)
|
||||
== 22.0
|
||||
)
|
||||
# 常规也有内在价值地板
|
||||
assert (
|
||||
resolve_option_close_bid(
|
||||
bid=0.2, mark=0.2, intrinsic=22.0, bypass_liquidity=False
|
||||
)
|
||||
== 22.0
|
||||
)
|
||||
# 买一高于内在价值,保留买一
|
||||
assert (
|
||||
resolve_option_close_bid(
|
||||
bid=25.0, mark=24.0, intrinsic=22.0, bypass_liquidity=True
|
||||
)
|
||||
== 25.0
|
||||
)
|
||||
# bypass 无买一,用标记与内在价值
|
||||
assert (
|
||||
resolve_option_close_bid(
|
||||
bid=None, mark=3.0, intrinsic=22.0, bypass_liquidity=True
|
||||
)
|
||||
== 22.0
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user