Add sim option expiry settlement so expired positions clear from current holdings.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-16 15:39:39 +08:00
parent b9544e53ee
commit 9effca9f05
+167 -10
View File
@@ -552,19 +552,183 @@ class SimBroker:
)
return result
def _index_px_for_option(
self,
exchange: Any,
inst_id: str,
*,
idx_cache: dict[str, float | None] | None = None,
) -> float | None:
"""到期结算/持仓展示用指数价:优先合约行情,否则 family 指数."""
from lib.exchange.okx_options_lib import (
fetch_index_price,
inst_family_from_inst_id,
quote_option_contract,
)
cache = idx_cache if idx_cache is not None else {}
if exchange is None or not inst_id:
return None
try:
q = quote_option_contract(exchange, inst_id)
if q.get("ok") and q.get("index_px") is not None:
return float(q["index_px"])
except Exception:
pass
family = inst_family_from_inst_id(inst_id) or ""
uly = family.replace("_UM", "") if family else ""
if not uly:
return None
if uly not in cache:
try:
cache[uly] = fetch_index_price(exchange, uly)
except Exception:
cache[uly] = None
return cache.get(uly)
def settle_expired_option_positions(
self,
exchange: Any = None,
*,
now_ms: int | None = None,
) -> list[dict[str, Any]]:
"""模拟盘到期结算:按指数内在价值兑付后删除本地仓(无实盘交割).
虚值兑付 0;实值 credit 交易账户 USDC.同时回写 options_trades 为 closed.
"""
import time
from lib.exchange.okx_options_lib import (
expiry_ms_from_inst_id,
option_fields_from_inst_id,
)
from lib.hedge_plan.hedge_plan_calc_lib import option_expiry_pnl
now = int(now_ms if now_ms is not None else time.time() * 1000)
settled: list[dict[str, Any]] = []
idx_cache: dict[str, float | None] = {}
for p in self.list_option_positions():
inst_id = str(p.get("inst_id") or "").strip()
if not inst_id:
continue
exp_ms = expiry_ms_from_inst_id(inst_id)
if exp_ms is None or now < int(exp_ms):
continue
opt_type, strike = option_fields_from_inst_id(inst_id)
if strike is None:
continue
spot = self._index_px_for_option(exchange, inst_id, idx_cache=idx_cache)
if spot is None:
# 无指数则本轮跳过,避免实值误按 0 结算
continue
sheets = float(p.get("sheets") or 0)
ct_mult = float(p.get("ct_mult") or 0.01)
prem = float(p.get("premium_paid_usdc") or 0)
pnl = float(
option_expiry_pnl(
opt_type=str(opt_type or "P"),
strike=float(strike),
spot=float(spot),
sheets=sheets,
ct_mult=ct_mult,
premium_paid=prem,
)
)
settle_recv = round(max(0.0, prem + pnl), 4)
o = (opt_type or "").strip().upper()
if o in ("C", "CALL"):
intrinsic_u = max(0.0, float(spot) - float(strike))
elif o in ("P", "PUT"):
intrinsic_u = max(0.0, float(strike) - float(spot))
else:
intrinsic_u = 0.0
conn = self.get_db()
try:
conn.execute("DELETE FROM sim_option_positions WHERE inst_id=?", (inst_id,))
try:
closed_at = _now()
conn.execute(
"""
UPDATE options_trades
SET status = 'closed',
close_quote = ?,
premium_received = ?,
realized_pnl = ?,
closed_at = COALESCE(closed_at, ?),
signal_note = CASE
WHEN signal_note IS NULL OR TRIM(signal_note) = ''
THEN '到期结算'
ELSE signal_note
END
WHERE inst_id = ? AND status = 'open'
""",
(
round(intrinsic_u, 6),
settle_recv,
round(pnl, 4),
closed_at,
inst_id,
),
)
except Exception:
pass
conn.commit()
finally:
conn.close()
if settle_recv > 1e-12:
self.wallets.credit_trading(
"USDC",
settle_recv,
kind="option_expiry",
note=f"expiry settle {inst_id} @{spot:g} recv={settle_recv}",
)
ord_id = f"sim-opt-exp-{uuid.uuid4().hex[:16]}"
self._store_option_order(
ord_id=ord_id,
inst_id=inst_id,
side="settle",
sheets=sheets,
avg_px=intrinsic_u,
)
try:
from lib.options.options_positions_lib import forget_close_gate_for_inst
forget_close_gate_for_inst(inst_id)
except Exception:
pass
settled.append(
{
"inst_id": inst_id,
"spot": spot,
"intrinsic": intrinsic_u,
"premium_received": settle_recv,
"realized_pnl": round(pnl, 4),
"ord_id": ord_id,
}
)
return settled
def option_positions_okx_rows(self, exchange: Any = None) -> list[dict[str, Any]]:
"""对齐 OKX positions 行字段, 供 format_position_row 使用.
模拟盘补充公开行情的 idxPx / markPx, 否则指数价与平掉回本均为空.
拉取前先结算已到期仓,避免虚值到期后一直挂在当前持仓.
"""
from lib.exchange.okx_options_lib import (
expiry_ms_from_inst_id,
fetch_index_price,
inst_family_from_inst_id,
option_fields_from_inst_id,
quote_option_contract,
)
try:
self.settle_expired_option_positions(exchange)
except Exception:
pass
rows: list[dict[str, Any]] = []
idx_cache: dict[str, float | None] = {}
for p in self.list_option_positions():
@@ -600,14 +764,7 @@ class SimBroker:
except Exception:
pass
if idx is None:
family = inst_family_from_inst_id(inst_id) or ""
uly = family.replace("_UM", "") if family else ""
if uly and uly not in idx_cache:
try:
idx_cache[uly] = fetch_index_price(exchange, uly)
except Exception:
idx_cache[uly] = None
idx = idx_cache.get(uly)
idx = self._index_px_for_option(exchange, inst_id, idx_cache=idx_cache)
eth = abs(sheets) * ct_mult
upl = (mark - entry) * eth