Add sim option expiry settlement so expired positions clear from current holdings.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+167
-10
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user