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
|
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]]:
|
def option_positions_okx_rows(self, exchange: Any = None) -> list[dict[str, Any]]:
|
||||||
"""对齐 OKX positions 行字段, 供 format_position_row 使用.
|
"""对齐 OKX positions 行字段, 供 format_position_row 使用.
|
||||||
|
|
||||||
模拟盘补充公开行情的 idxPx / markPx, 否则指数价与平掉回本均为空.
|
模拟盘补充公开行情的 idxPx / markPx, 否则指数价与平掉回本均为空.
|
||||||
|
拉取前先结算已到期仓,避免虚值到期后一直挂在当前持仓.
|
||||||
"""
|
"""
|
||||||
from lib.exchange.okx_options_lib import (
|
from lib.exchange.okx_options_lib import (
|
||||||
expiry_ms_from_inst_id,
|
expiry_ms_from_inst_id,
|
||||||
fetch_index_price,
|
|
||||||
inst_family_from_inst_id,
|
|
||||||
option_fields_from_inst_id,
|
option_fields_from_inst_id,
|
||||||
quote_option_contract,
|
quote_option_contract,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.settle_expired_option_positions(exchange)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
rows: list[dict[str, Any]] = []
|
rows: list[dict[str, Any]] = []
|
||||||
idx_cache: dict[str, float | None] = {}
|
idx_cache: dict[str, float | None] = {}
|
||||||
for p in self.list_option_positions():
|
for p in self.list_option_positions():
|
||||||
@@ -600,14 +764,7 @@ class SimBroker:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
if idx is None:
|
if idx is None:
|
||||||
family = inst_family_from_inst_id(inst_id) or ""
|
idx = self._index_px_for_option(exchange, inst_id, idx_cache=idx_cache)
|
||||||
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)
|
|
||||||
|
|
||||||
eth = abs(sheets) * ct_mult
|
eth = abs(sheets) * ct_mult
|
||||||
upl = (mark - entry) * eth
|
upl = (mark - entry) * eth
|
||||||
|
|||||||
Reference in New Issue
Block a user