Settle expiry options at intrinsic value like live exchange.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+69
-53
@@ -13,6 +13,7 @@ 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_expiry_settle,
|
||||
option_fill,
|
||||
option_intrinsic,
|
||||
perp_fill,
|
||||
@@ -329,9 +330,9 @@ class Matcher:
|
||||
def close_group(self, *, reason: str, bypass_liquidity: bool = False) -> CloseResult:
|
||||
"""
|
||||
全平一组。默认校验期权买一深度 + 买一/标记偏差(默认≤30%)。
|
||||
bypass_liquidity=True:紧急/到期可绕过深度闸门;平仓价取 max(买一, 标记, 内在价值),
|
||||
避免到期垃圾盘口把实值期权按近零价卖掉。
|
||||
成交顺序:先平期权(薄)→ 再瞬时平永续(对冲先留着);永续盘口失败则回滚期权入账。
|
||||
reason=expiry:对齐实盘,期权按标的结算价的内在价值入账(不吃盘口)。
|
||||
bypass_liquidity=True(紧急):绕过深度闸门,价取 max(买一, 标记, 内在价值)。
|
||||
成交顺序:先平期权 → 再瞬时平永续;永续盘口失败则回滚期权入账。
|
||||
"""
|
||||
s = get_settings()
|
||||
pos = self.current_position()
|
||||
@@ -349,12 +350,6 @@ class Matcher:
|
||||
oq = get_exchange().quote(option_inst_id) or (
|
||||
snap.call if option_side == "call" else snap.put
|
||||
)
|
||||
if not oq:
|
||||
return CloseResult(
|
||||
ok=False,
|
||||
detail="期权盘口不可用",
|
||||
liquidity_wait=not bypass_liquidity,
|
||||
)
|
||||
|
||||
ct_mult = self._ct_mult(option_inst_id)
|
||||
need_eth = float(pos["option_qty_eth"] or s.option_qty_eth)
|
||||
@@ -362,7 +357,6 @@ class Matcher:
|
||||
"close_bid_mark_max_pct", s.close_bid_mark_max_pct
|
||||
)
|
||||
|
||||
close_bid = oq.bid
|
||||
strike = self._group_strike(group_id, option_inst_id)
|
||||
spot = self._close_spot_px(snap)
|
||||
intrinsic: float | None = None
|
||||
@@ -371,59 +365,81 @@ class Matcher:
|
||||
option_side=option_side, strike=strike, spot=spot
|
||||
)
|
||||
|
||||
if not bypass_liquidity:
|
||||
if close_bid is None:
|
||||
return self._liquidity_wait(group_id, "期权买一不可用")
|
||||
if not bid_covers_eth(
|
||||
bid_sz_contracts=oq.bid_sz,
|
||||
ct_mult=ct_mult,
|
||||
need_eth=need_eth,
|
||||
):
|
||||
return self._liquidity_wait(group_id, "期权买一流动性不足")
|
||||
ok_dev, why = bid_mark_ok(
|
||||
bid=close_bid, mark=oq.mark_px, max_dev_pct=max_dev
|
||||
)
|
||||
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:
|
||||
# 到期/紧急:买一/标记可能枯死,用 max(买一, 标记, 内在价值)
|
||||
resolved = resolve_option_close_bid(
|
||||
bid=close_bid,
|
||||
mark=oq.mark_px,
|
||||
intrinsic=intrinsic,
|
||||
bypass_liquidity=True,
|
||||
)
|
||||
if resolved is None:
|
||||
fee_rate = self._fee_rate()
|
||||
is_expiry = reason == "expiry"
|
||||
|
||||
if is_expiry:
|
||||
# 实盘到期:直接按内在价值结算,不依赖盘口
|
||||
if intrinsic is None:
|
||||
return CloseResult(
|
||||
ok=False,
|
||||
detail="紧急全平失败:无买一/标记/内在价值",
|
||||
detail="到期结算失败:缺少行权价或标的结算价",
|
||||
)
|
||||
close_bid = resolved
|
||||
of = option_expiry_settle(
|
||||
intrinsic=float(intrinsic),
|
||||
qty_eth=float(pos["option_qty_eth"]),
|
||||
fee_rate=fee_rate,
|
||||
)
|
||||
close_bid = float(intrinsic)
|
||||
else:
|
||||
if not oq:
|
||||
return CloseResult(
|
||||
ok=False,
|
||||
detail="期权盘口不可用",
|
||||
liquidity_wait=not bypass_liquidity,
|
||||
)
|
||||
close_bid = oq.bid
|
||||
if not bypass_liquidity:
|
||||
if close_bid is None:
|
||||
return self._liquidity_wait(group_id, "期权买一不可用")
|
||||
if not bid_covers_eth(
|
||||
bid_sz_contracts=oq.bid_sz,
|
||||
ct_mult=ct_mult,
|
||||
need_eth=need_eth,
|
||||
):
|
||||
return self._liquidity_wait(group_id, "期权买一流动性不足")
|
||||
ok_dev, why = bid_mark_ok(
|
||||
bid=close_bid, mark=oq.mark_px, max_dev_pct=max_dev
|
||||
)
|
||||
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:
|
||||
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
|
||||
of = option_fill(
|
||||
action="close",
|
||||
bid=float(close_bid),
|
||||
ask=float(oq.ask or close_bid),
|
||||
qty_eth=float(pos["option_qty_eth"]),
|
||||
fee_rate=fee_rate,
|
||||
)
|
||||
|
||||
fee_rate = self._fee_rate()
|
||||
perp_side = str(pos["perp_side"])
|
||||
perp_qty = float(pos["perp_qty_eth"])
|
||||
opt_qty = float(pos["option_qty_eth"])
|
||||
perp_entry = float(pos["perp_entry_px"])
|
||||
opt_entry = float(pos["option_entry_px"])
|
||||
|
||||
# 1) 先平期权(买一流动性差);永续对冲暂留
|
||||
of = option_fill(
|
||||
action="close",
|
||||
bid=float(close_bid),
|
||||
ask=float(oq.ask or close_bid),
|
||||
qty_eth=opt_qty,
|
||||
fee_rate=fee_rate,
|
||||
)
|
||||
# 1) 先平期权;永续对冲暂留
|
||||
opt_pnl = (of.fill_px - opt_entry) * opt_qty
|
||||
opt_cash = of.notional - of.fee
|
||||
self.ledger.apply_cash(
|
||||
|
||||
@@ -29,6 +29,21 @@ def option_intrinsic(*, option_side: str, strike: float, spot: float) -> float:
|
||||
return 0.0
|
||||
|
||||
|
||||
def option_expiry_settle(
|
||||
*,
|
||||
intrinsic: float,
|
||||
qty_eth: float,
|
||||
fee_rate: float,
|
||||
) -> PriceResult:
|
||||
"""到期结算:按内在价值入账(对齐实盘),无买卖价差滑点,仅扣手续费。"""
|
||||
base = max(float(intrinsic), 0.0)
|
||||
fill = base
|
||||
f = float(fee_rate)
|
||||
notional = abs(fill * float(qty_eth))
|
||||
fee = notional * f
|
||||
return PriceResult(base_px=base, fill_px=fill, fee=fee, slip=0.0, notional=notional)
|
||||
|
||||
|
||||
def resolve_option_close_bid(
|
||||
*,
|
||||
bid: float | None,
|
||||
@@ -37,8 +52,9 @@ def resolve_option_close_bid(
|
||||
bypass_liquidity: bool,
|
||||
) -> float | None:
|
||||
"""
|
||||
平仓用买一价;多头卖出不得低于内在价值(SIM 防到期垃圾盘口)。
|
||||
bypass 时:买一缺失可用标记/内在价值兜底。
|
||||
非到期平仓用买一价;多头卖出不得低于内在价值(SIM)。
|
||||
紧急 bypass:max(买一, 标记, 内在价值)。
|
||||
到期请用 option_expiry_settle,不要走本函数。
|
||||
"""
|
||||
candidates: list[float] = []
|
||||
if bid is not None and bid >= 0:
|
||||
@@ -49,8 +65,6 @@ def resolve_option_close_bid(
|
||||
candidates.append(float(intrinsic))
|
||||
if not candidates:
|
||||
return None
|
||||
# 常规:有买一时,仍用 max(买一, 内在价值) 抬到合理底价
|
||||
# bypass:max(买一, 标记, 内在价值)
|
||||
if bypass_liquidity:
|
||||
return max(candidates)
|
||||
if bid is None:
|
||||
|
||||
@@ -145,13 +145,24 @@ def test_expiry_close() -> None:
|
||||
|
||||
|
||||
def test_option_intrinsic_and_close_bid_floor() -> None:
|
||||
from app.sim.pricing import option_intrinsic, resolve_option_close_bid
|
||||
from app.sim.pricing import (
|
||||
option_expiry_settle,
|
||||
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
|
||||
# 到期:严格按内在价值,无滑点
|
||||
settled = option_expiry_settle(intrinsic=22.0, qty_eth=2.0, fee_rate=0.0005)
|
||||
assert settled.fill_px == 22.0
|
||||
assert settled.slip == 0.0
|
||||
assert settled.notional == 44.0
|
||||
assert abs(settled.fee - 44.0 * 0.0005) < 1e-12
|
||||
|
||||
# 紧急垃圾买一 0.2,内在价值 22 → 抬到 22
|
||||
assert (
|
||||
resolve_option_close_bid(
|
||||
bid=0.2, mark=0.2, intrinsic=22.0, bypass_liquidity=True
|
||||
|
||||
+4
-2
@@ -134,7 +134,9 @@
|
||||
- 买一相对标记偏差默认 ≤ **30%**(`close_bid_mark_max_pct`);
|
||||
- 不满足 → `liquidity_wait`,继续等待,不改开仓。
|
||||
|
||||
**到期 / 紧急全平**:绕过上述闸门;期权平仓价取 **max(买一, 标记, 内在价值)**,避免到期盘口枯死把实值期权按近零价结算。
|
||||
**到期自动全平**:对齐实盘,期权按标的结算价计算 **内在价值** 入账(不吃盘口、无价差滑点);永续仍市价平掉。策略暂停时仍执行。
|
||||
|
||||
**紧急全平**:绕过流动性闸门;期权价取 max(买一, 标记, 内在价值)。
|
||||
|
||||
### 4.4 其它平仓入口
|
||||
|
||||
@@ -257,4 +259,4 @@
|
||||
|------|------|
|
||||
| 2026-07-25 | 初稿:对齐当前开平仓、周末跳过、到期全平、净盈利口径与资金建议 |
|
||||
| 2026-07-25 | 平仓顺序改为先期权后永续(与开仓同理:薄腿优先) |
|
||||
| 2026-07-26 | 到期/紧急平仓:期权价不低于内在价值,修复垃圾盘口错杀实值 |
|
||||
| 2026-07-26 | 到期按内在价值结算(对齐实盘);紧急平仓仍用 max(买一,标记,内在价值) |
|
||||
|
||||
@@ -50,16 +50,17 @@ perp_fill = float(p_close["fill_px"])
|
||||
spot = perp_fill / (1.0 + FEE)
|
||||
intrinsic = max(spot - strike, 0.0) if str(g["option_side"])=="call" else max(strike - spot, 0.0)
|
||||
old_base = float(o_close["base_px"] or o_close["fill_px"])
|
||||
new_base = max(old_base, intrinsic)
|
||||
if abs(new_base - old_base) < 1e-9:
|
||||
# 到期对齐实盘:严格内在价值,无盘口滑点
|
||||
new_base = intrinsic
|
||||
new_fill = intrinsic
|
||||
if abs(new_base - old_base) < 1e-9 and abs(float(o_close["fill_px"]) - new_fill) < 1e-9:
|
||||
print("already ok", old_base, intrinsic)
|
||||
raise SystemExit(0)
|
||||
|
||||
qty = float(o_close["qty_eth"])
|
||||
new_fill = new_base * (1.0 - FEE)
|
||||
new_notional = new_fill * qty
|
||||
new_fee = new_notional * FEE
|
||||
new_slip = abs(new_fill - new_base) * qty
|
||||
new_slip = 0.0
|
||||
old_cash = float(o_close["notional"]) - float(o_close["fee"])
|
||||
new_cash = new_notional - new_fee
|
||||
cash_delta = new_cash - old_cash
|
||||
|
||||
Reference in New Issue
Block a user